diff options
author | Valerij Fredriksen <valerijf@vespa.ai> | 2023-10-26 11:58:28 +0200 |
---|---|---|
committer | Valerij Fredriksen <valerijf@vespa.ai> | 2023-10-26 12:40:34 +0200 |
commit | caae29b85132997369561fcd12ec41f7a1b7ef82 (patch) | |
tree | cfe09c2521f06cc9fe46179b9eb8c482da4974e6 | |
parent | b3a7676e2352dabd6902b51f1619a9353fe1058e (diff) |
Remove /billing/v1
6 files changed, 5 insertions, 956 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java index 52900f83203..2afe7417787 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java @@ -72,22 +72,6 @@ enum PathGroup { "/application/v4/tenant/{tenant}/archive-access/aws", "/application/v4/tenant/{tenant}/archive-access/gcp"), - - billingToken(Matcher.tenant, - "/billing/v1/tenant/{tenant}/token"), - - billingInstrument(Matcher.tenant, - "/billing/v1/tenant/{tenant}/instrument/{*}"), - - billingPlan(Matcher.tenant, - "/billing/v1/tenant/{tenant}/plan/{*}"), - - billingCollection(Matcher.tenant, - "/billing/v1/tenant/{tenant}/collection/{*}"), - - billingList(Matcher.tenant, - "/billing/v1/tenant/{tenant}/billing/{*}"), - billing(Matcher.tenant, "/billing/v2/tenant/{tenant}/{*}"), @@ -247,11 +231,6 @@ enum PathGroup { /** Paths used for receiving payment callbacks */ paymentProcessor("/payment/notification"), - /** Paths used for invoice management */ - hostedAccountant("/billing/v1/invoice/{*}", - "/billing/v1/billing", - "/billing/v1/plans"), - /** Path used for listing endpoint certificate request and re-requesting endpoint certificates */ endpointCertificates("/endpointcertificates/"), @@ -322,20 +301,12 @@ enum PathGroup { static Set<PathGroup> operatorRestrictedPaths() { var paths = billingPathsNoToken(); - paths.add(PathGroup.billingToken); paths.add(accessRequestApproval); return paths; } static Set<PathGroup> billingPathsNoToken() { - return EnumSet.of( - PathGroup.billingCollection, - PathGroup.billingInstrument, - PathGroup.billingList, - PathGroup.billingPlan, - PathGroup.billing, - PathGroup.hostedAccountant - ); + return EnumSet.of(PathGroup.billing); } /** Returns whether this group matches path in given context */ diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java index 6b5130cf2e5..373af30e475 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java @@ -26,10 +26,7 @@ enum Policy { .in(SystemName.all()), Privilege.grant(Action.read) .on(PathGroup.billingPathsNoToken()) - .in(SystemName.all()), - Privilege.grant(Action.read) - .on(PathGroup.billingToken) - .in(SystemName.PublicCd)), + .in(SystemName.all())), /** Full access to everything. */ supporter(Privilege.grant(Action.read) @@ -155,40 +152,14 @@ enum Policy { .on(PathGroup.paymentProcessor) .in(SystemName.PublicCd)), - /** Read your own instrument information */ - paymentInstrumentRead(Privilege.grant(Action.read) - .on(PathGroup.billingInstrument) - .in(SystemName.PublicCd, SystemName.Public)), - - /** Ability to update tenant payment instrument */ - paymentInstrumentUpdate(Privilege.grant(Action.update) - .on(PathGroup.billingInstrument) - .in(SystemName.PublicCd, SystemName.Public)), - - /** Ability to remove your own payment instrument */ - paymentInstrumentDelete(Privilege.grant(Action.delete) - .on(PathGroup.billingInstrument) - .in(SystemName.PublicCd, SystemName.Public)), - - /** Get the token to view instrument form */ - paymentInstrumentCreate(Privilege.grant(Action.read) - .on(PathGroup.billingToken) - .in(SystemName.PublicCd, SystemName.Public)), - /** Ability to update tenant payment instrument */ planUpdate(Privilege.grant(Action.update) - .on(PathGroup.billingPlan, PathGroup.billing) - .in(SystemName.PublicCd, SystemName.Public)), - - /** Ability to update tenant collection method */ - collectionMethodUpdate(Privilege.grant(Action.update) - .on(PathGroup.billingCollection) + .on(PathGroup.billing) .in(SystemName.PublicCd, SystemName.Public)), - /** Read the generated bills */ billingInformationRead(Privilege.grant(Action.read) - .on(PathGroup.billingList, PathGroup.billing) + .on(PathGroup.billing) .in(SystemName.PublicCd, SystemName.Public)), accessRequests(Privilege.grant(Action.all()) @@ -197,7 +168,7 @@ enum Policy { /** Invoice management */ hostedAccountant(Privilege.grant(Action.all()) - .on(PathGroup.hostedAccountant, PathGroup.accountant, PathGroup.userSearch) + .on(PathGroup.accountant, PathGroup.userSearch) .in(SystemName.PublicCd, SystemName.Public)), /** Listing endpoint certificates and re-requesting certificates */ diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java index d57e38df239..31c8560c908 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java @@ -43,8 +43,6 @@ public enum RoleDefinition { Policy.applicationRead, Policy.deploymentRead, Policy.publicRead, - Policy.paymentInstrumentRead, - Policy.paymentInstrumentDelete, Policy.billingInformationRead, Policy.horizonProxyOperations), @@ -56,8 +54,6 @@ public enum RoleDefinition { Policy.developmentDeployment, Policy.keyManagement, Policy.submission, - Policy.paymentInstrumentRead, - Policy.paymentInstrumentDelete, Policy.billingInformationRead, Policy.secretStoreOperations, Policy.dataplaneToken), @@ -72,7 +68,6 @@ public enum RoleDefinition { Policy.tenantArchiveAccessManagement, Policy.applicationManager, Policy.keyRevokal, - Policy.paymentInstrumentRead, Policy.billingInformationRead, Policy.accessRequests ), @@ -99,7 +94,6 @@ public enum RoleDefinition { paymentProcessor(Policy.paymentProcessor), hostedAccountant(Policy.hostedAccountant, - Policy.collectionMethodUpdate, Policy.planUpdate, Policy.tenantUpdate); diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/RoleTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/RoleTest.java index 24539d7c158..c8020666906 100644 --- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/RoleTest.java +++ b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/RoleTest.java @@ -8,7 +8,6 @@ import org.junit.jupiter.api.Test; import java.net.URI; import java.util.List; -import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -146,139 +145,6 @@ public class RoleTest { } } - @Test - void payment_instrument() { - URI paymentInstrumentUri = URI.create("/billing/v1/tenant/t1/instrument/foobar"); - URI tenantPaymentInstrumentUri = URI.create("/billing/v1/tenant/t1/instrument"); - URI tokenUri = URI.create("/billing/v1/tenant/t1/token"); - - Role user = Role.reader(TenantName.from("t1")); - assertTrue(publicCdEnforcer.allows(user, Action.read, paymentInstrumentUri)); - assertTrue(publicCdEnforcer.allows(user, Action.delete, paymentInstrumentUri)); - assertFalse(publicCdEnforcer.allows(user, Action.update, tenantPaymentInstrumentUri)); - assertFalse(publicCdEnforcer.allows(user, Action.read, tokenUri)); - - Role developer = Role.developer(TenantName.from("t1")); - assertTrue(publicCdEnforcer.allows(developer, Action.read, paymentInstrumentUri)); - assertTrue(publicCdEnforcer.allows(developer, Action.delete, paymentInstrumentUri)); - assertFalse(publicCdEnforcer.allows(developer, Action.update, tenantPaymentInstrumentUri)); - assertFalse(publicCdEnforcer.allows(developer, Action.read, tokenUri)); - - Role admin = Role.administrator(TenantName.from("t1")); - assertTrue(publicCdEnforcer.allows(admin, Action.read, paymentInstrumentUri)); - assertFalse(publicCdEnforcer.allows(admin, Action.delete, paymentInstrumentUri)); - assertFalse(publicCdEnforcer.allows(admin, Action.update, tenantPaymentInstrumentUri)); - assertFalse(publicCdEnforcer.allows(admin, Action.read, tokenUri)); - } - - @Test - void billing_tenant() { - URI billing = URI.create("/billing/v1/tenant/t1/billing"); - - Role user = Role.reader(TenantName.from("t1")); - Role developer = Role.developer(TenantName.from("t1")); - Role admin = Role.administrator(TenantName.from("t1")); - - Stream.of(user, developer, admin).forEach(role -> { - assertTrue(publicCdEnforcer.allows(role, Action.read, billing)); - assertFalse(publicCdEnforcer.allows(role, Action.update, billing)); - assertFalse(publicCdEnforcer.allows(role, Action.delete, billing)); - assertFalse(publicCdEnforcer.allows(role, Action.create, billing)); - }); - - } - - @Test - void billing_test() { - var tester = new EnforcerTester(publicEnforcer); - - var accountant = Role.hostedAccountant(); - var operator = Role.hostedOperator(); - var reader = Role.reader(TenantName.from("t1")); - var developer = Role.developer(TenantName.from("t1")); - var admin = Role.administrator(TenantName.from("t1")); - var otherAdmin = Role.administrator(TenantName.from("t2")); - - tester.on("/billing/v1/tenant/t1/token") - .assertAction(accountant) - .assertAction(operator) - .assertAction(reader) - .assertAction(developer) - .assertAction(otherAdmin); - - tester.on("/billing/v1/tenant/t1/instrument") - .assertAction(accountant) - .assertAction(operator, Action.read) - .assertAction(reader, Action.read, Action.delete) - .assertAction(developer, Action.read, Action.delete) - .assertAction(admin, Action.read) - .assertAction(otherAdmin); - - tester.on("/billing/v1/tenant/t1/instrument/i1") - .assertAction(accountant) - .assertAction(operator, Action.read) - .assertAction(reader, Action.read, Action.delete) - .assertAction(developer, Action.read, Action.delete) - .assertAction(admin, Action.read) - .assertAction(otherAdmin); - - tester.on("/billing/v1/tenant/t1/billing") - .assertAction(accountant) - .assertAction(operator, Action.read) - .assertAction(reader, Action.read) - .assertAction(developer, Action.read) - .assertAction(admin, Action.read) - .assertAction(otherAdmin); - - tester.on("/billing/v1/tenant/t1/plan") - .assertAction(accountant, Action.update) - .assertAction(operator, Action.read) - .assertAction(reader) - .assertAction(developer) - .assertAction(admin) - .assertAction(otherAdmin); - - tester.on("/billing/v1/tenant/t1/collection") - .assertAction(accountant, Action.update) - .assertAction(operator, Action.read) - .assertAction(reader) - .assertAction(developer) - .assertAction(admin) - .assertAction(otherAdmin); - - tester.on("/billing/v1/billing") - .assertAction(accountant, Action.create, Action.read, Action.update, Action.delete) - .assertAction(operator, Action.read) - .assertAction(reader) - .assertAction(developer) - .assertAction(admin) - .assertAction(otherAdmin); - - tester.on("/billing/v1/invoice/tenant/t1/line-item") - .assertAction(accountant, Action.create, Action.read, Action.update, Action.delete) - .assertAction(operator, Action.read) - .assertAction(reader) - .assertAction(developer) - .assertAction(admin) - .assertAction(otherAdmin); - - tester.on("/billing/v1/invoice") - .assertAction(accountant, Action.create, Action.read, Action.update, Action.delete) - .assertAction(operator, Action.read) - .assertAction(reader) - .assertAction(developer) - .assertAction(admin) - .assertAction(otherAdmin); - - tester.on("/billing/v1/invoice/i1/status") - .assertAction(accountant, Action.create, Action.read, Action.update, Action.delete) - .assertAction(operator, Action.read) - .assertAction(reader) - .assertAction(developer) - .assertAction(admin) - .assertAction(otherAdmin); - } - private static class EnforcerTester { private final Enforcer enforcer; private final URI resource; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java deleted file mode 100644 index 6dc29ebe08c..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java +++ /dev/null @@ -1,517 +0,0 @@ -// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.controller.restapi.billing; - -import com.yahoo.config.provision.TenantName; -import com.yahoo.container.jdisc.HttpRequest; -import com.yahoo.container.jdisc.HttpResponse; -import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; -import com.yahoo.restapi.ErrorResponse; -import com.yahoo.restapi.JacksonJsonResponse; -import com.yahoo.restapi.MessageResponse; -import com.yahoo.restapi.Path; -import com.yahoo.restapi.SlimeJsonResponse; -import com.yahoo.restapi.StringResponse; -import com.yahoo.slime.Cursor; -import com.yahoo.slime.Inspector; -import com.yahoo.slime.Slime; -import com.yahoo.slime.SlimeUtils; -import com.yahoo.vespa.hosted.controller.ApplicationController; -import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.TenantController; -import com.yahoo.vespa.hosted.controller.api.integration.billing.Bill; -import com.yahoo.vespa.hosted.controller.api.integration.billing.BillStatus; -import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController; -import com.yahoo.vespa.hosted.controller.api.integration.billing.CollectionMethod; -import com.yahoo.vespa.hosted.controller.api.integration.billing.InstrumentOwner; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PaymentInstrument; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry; -import com.yahoo.vespa.hosted.controller.api.integration.billing.StatusHistory; -import com.yahoo.vespa.hosted.controller.api.role.Role; -import com.yahoo.vespa.hosted.controller.api.role.SecurityContext; -import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses; -import com.yahoo.vespa.hosted.controller.tenant.Tenant; -import com.yahoo.yolean.Exceptions; - -import java.io.IOException; -import java.math.BigDecimal; -import java.security.Principal; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.Executor; - -/** - * @author andreer - * @author olaa - */ -public class BillingApiHandler extends ThreadedHttpRequestHandler { - - private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - - private final BillingController billingController; - private final ApplicationController applicationController; - private final TenantController tenantController; - private final PlanRegistry planRegistry; - - public BillingApiHandler(Executor executor, - Controller controller) { - super(executor); - this.billingController = controller.serviceRegistry().billingController(); - this.planRegistry = controller.serviceRegistry().planRegistry(); - this.applicationController = controller.applications(); - this.tenantController = controller.tenants(); - } - - @Override - public HttpResponse handle(HttpRequest request) { - try { - Optional<String> userId = Optional.ofNullable(request.getJDiscRequest().getUserPrincipal()).map(Principal::getName); - if (userId.isEmpty()) - return ErrorResponse.unauthorized("Must be authenticated to use this API"); - - Path path = new Path(request.getUri()); - return switch (request.getMethod()) { - case GET -> handleGET(request, path, userId.get()); - case PATCH -> handlePATCH(request, path, userId.get()); - case DELETE -> handleDELETE(path, userId.get()); - case POST -> handlePOST(path, request, userId.get()); - default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported"); - }; - } - catch (IllegalArgumentException e) { - return ErrorResponse.badRequest(Exceptions.toMessageString(e)); - } catch (Exception e) { - return ErrorResponses.logThrowing(request, log, e); - } - } - - private HttpResponse handleGET(HttpRequest request, Path path, String userId) { - if (path.matches("/billing/v1/tenant/{tenant}/token")) return getToken(path.get("tenant"), userId); - if (path.matches("/billing/v1/tenant/{tenant}/instrument")) return getInstruments(path.get("tenant"), userId); - if (path.matches("/billing/v1/tenant/{tenant}/billing")) return getBilling(path.get("tenant"), request.getProperty("until")); - if (path.matches("/billing/v1/tenant/{tenant}/plan")) return getPlan(path.get("tenant")); - if (path.matches("/billing/v1/billing")) return getBillingAllTenants(request.getProperty("until")); - if (path.matches("/billing/v1/invoice/export")) return getAllBills(); - if (path.matches("/billing/v1/invoice/tenant/{tenant}/line-item")) return getLineItems(path.get("tenant")); - if (path.matches("/billing/v1/plans")) return getPlans(); - return ErrorResponse.notFoundError("Nothing at " + path); - } - - private HttpResponse getAllBills() { - var bills = billingController.getBills(); - var headers = new String[]{ "ID", "Tenant", "From", "To", "CpuHours", "MemoryHours", "DiskHours", "Cpu", "Memory", "Disk", "Additional" }; - var rows = bills.stream() - .map(bill -> { - return new Object[] { - bill.id().value(), bill.tenant().value(), - bill.getStartDate().format(DateTimeFormatter.ISO_LOCAL_DATE), - bill.getEndDate().format(DateTimeFormatter.ISO_LOCAL_DATE), - bill.sumCpuHours(), bill.sumMemoryHours(), bill.sumDiskHours(), - bill.sumCpuCost(), bill.sumMemoryCost(), bill.sumDiskCost(), - bill.sumAdditionalCost() - }; - }) - .toList(); - return new CsvResponse(headers, rows); - } - - private HttpResponse handlePATCH(HttpRequest request, Path path, String userId) { - if (path.matches("/billing/v1/tenant/{tenant}/instrument")) return patchActiveInstrument(request, path.get("tenant"), userId); - if (path.matches("/billing/v1/tenant/{tenant}/plan")) return patchPlan(request, path.get("tenant")); - if (path.matches("/billing/v1/tenant/{tenant}/collection")) return patchCollectionMethod(request, path.get("tenant")); - return ErrorResponse.notFoundError("Nothing at " + path); - - } - - private HttpResponse handleDELETE(Path path, String userId) { - if (path.matches("/billing/v1/tenant/{tenant}/instrument/{instrument}")) return deleteInstrument(path.get("tenant"), userId, path.get("instrument")); - if (path.matches("/billing/v1/invoice/line-item/{line-item-id}")) return deleteLineItem(path.get("line-item-id")); - return ErrorResponse.notFoundError("Nothing at " + path); - - } - - private HttpResponse handlePOST(Path path, HttpRequest request, String userId) { - if (path.matches("/billing/v1/invoice")) return createBill(request, userId); - if (path.matches("/billing/v1/invoice/{invoice-id}/status")) return setBillStatus(request, path.get("invoice-id"), userId); - if (path.matches("/billing/v1/invoice/tenant/{tenant}/line-item")) return addLineItem(request, path.get("tenant"), userId); - return ErrorResponse.notFoundError("Nothing at " + path); - - } - - private HttpResponse getPlan(String tenant) { - var plan = billingController.getPlan(TenantName.from(tenant)); - var slime = new Slime(); - var root = slime.setObject(); - root.setString("tenant", tenant); - root.setString("plan", plan.value()); - return new SlimeJsonResponse(slime); - } - - private HttpResponse patchPlan(HttpRequest request, String tenant) { - var tenantName = TenantName.from(tenant); - var slime = inspectorOrThrow(request); - var planId = PlanId.from(slime.field("plan").asString()); - var roles = requestRoles(request); - var isAccountant = roles.contains(Role.hostedAccountant()); - - var hasDeployments = hasDeployments(tenantName); - var result = billingController.setPlan(tenantName, planId, hasDeployments, isAccountant); - - if (result.isSuccess()) - return new StringResponse("Plan: " + planId.value()); - - return ErrorResponse.forbidden(result.getErrorMessage().orElse("Invalid plan change")); - } - - private HttpResponse patchCollectionMethod(HttpRequest request, String tenant) { - var tenantName = TenantName.from(tenant); - var slime = inspectorOrThrow(request); - var newMethod = slime.field("collection").valid() ? - slime.field("collection").asString().toUpperCase() : - slime.field("collectionMethod").asString().toUpperCase(); - if (newMethod.isEmpty()) return ErrorResponse.badRequest("No collection method specified"); - - try { - var result = billingController.setCollectionMethod(tenantName, CollectionMethod.valueOf(newMethod)); - if (result.isSuccess()) - return new StringResponse("Collection method updated to " + newMethod); - - return ErrorResponse.forbidden(result.getErrorMessage().orElse("Invalid collection method change")); - } catch (IllegalArgumentException iea){ - return ErrorResponse.badRequest("Invalid collection method: " + newMethod); - } - } - - private HttpResponse getBillingAllTenants(String until) { - try { - var untilDate = untilParameter(until); - var uncommittedBills = billingController.createUncommittedBills(untilDate); - - var slime = new Slime(); - var root = slime.setObject(); - root.setString("until", untilDate.format(DateTimeFormatter.ISO_DATE)); - var tenants = root.setArray("tenants"); - - tenantController.asList().stream().sorted(Comparator.comparing(Tenant::name)).forEach(tenant -> { - var bill = uncommittedBills.get(tenant.name()); - var tc = tenants.addObject(); - tc.setString("tenant", tenant.name().value()); - getPlanForTenant(tc, tenant.name()); - getCollectionForTenant(tc, tenant.name()); - renderCurrentUsage(tc.setObject("current"), bill); - renderAdditionalItems(tc.setObject("additional").setArray("items"), billingController.getUnusedLineItems(tenant.name())); - - billingController.getDefaultInstrument(tenant.name()).ifPresent(card -> - renderInstrument(tc.setObject("payment"), card) - ); - }); - - return new SlimeJsonResponse(slime); - } catch (DateTimeParseException e) { - return ErrorResponse.badRequest("Could not parse date: " + until); - } - } - - private void getCollectionForTenant(Cursor tc, TenantName tenant) { - var collection = billingController.getCollectionMethod(tenant); - tc.setString("collection", collection.name()); - } - - private HttpResponse addLineItem(HttpRequest request, String tenant, String userId) { - Inspector inspector = inspectorOrThrow(request); - - Optional<Bill.Id> billId = SlimeUtils.optionalString(inspector.field("billId")).map(Bill.Id::of); - - billingController.addLineItem( - TenantName.from(tenant), - getInspectorFieldOrThrow(inspector, "description"), - new BigDecimal(getInspectorFieldOrThrow(inspector, "amount")), - billId, - userId); - - return new MessageResponse("Added line item for tenant " + tenant); - } - - private HttpResponse setBillStatus(HttpRequest request, String billId, String userId) { - Inspector inspector = inspectorOrThrow(request); - String status = getInspectorFieldOrThrow(inspector, "status"); - billingController.updateBillStatus(Bill.Id.of(billId), userId, BillStatus.from(status)); - return new MessageResponse("Updated status of invoice " + billId); - } - - private HttpResponse createBill(HttpRequest request, String userId) { - Inspector inspector = inspectorOrThrow(request); - TenantName tenantName = TenantName.from(getInspectorFieldOrThrow(inspector, "tenant")); - - LocalDate startDate = LocalDate.parse(getInspectorFieldOrThrow(inspector, "startTime")); - LocalDate endDate = LocalDate.parse(getInspectorFieldOrThrow(inspector, "endTime")); - - var billId = billingController.createBillForPeriod(tenantName, startDate, endDate, userId); - - Slime slime = new Slime(); - Cursor root = slime.setObject(); - root.setString("message", "Created invoice with ID " + billId.value()); - root.setString("id", billId.value()); - return new SlimeJsonResponse(slime); - } - - private HttpResponse getInstruments(String tenant, String userId) { - var instrumentListResponse = billingController.listInstruments(TenantName.from(tenant), userId); - return new JacksonJsonResponse<>(200, instrumentListResponse); - } - - private HttpResponse getToken(String tenant, String userId) { - return new StringResponse(billingController.createClientToken(tenant, userId)); - } - - private HttpResponse getBilling(String tenant, String until) { - try { - var untilDate = untilParameter(until); - var tenantId = TenantName.from(tenant); - var slimeResponse = new Slime(); - var root = slimeResponse.setObject(); - - root.setString("until", untilDate.format(DateTimeFormatter.ISO_DATE)); - - getPlanForTenant(root, tenantId); - renderCurrentUsage(root.setObject("current"), getCurrentUsageForTenant(tenantId, untilDate)); - renderAdditionalItems(root.setObject("additional").setArray("items"), billingController.getUnusedLineItems(tenantId)); - renderBills(root.setArray("bills"), getBillsForTenant(tenantId)); - - billingController.getDefaultInstrument(tenantId).ifPresent( card -> - renderInstrument(root.setObject("payment"), card) - ); - - root.setString("collection", billingController.getCollectionMethod(tenantId).name()); - return new SlimeJsonResponse(slimeResponse); - } catch (DateTimeParseException e) { - return ErrorResponse.badRequest("Could not parse date: " + until); - } - } - - private HttpResponse getPlans() { - var slime = new Slime(); - var root = slime.setObject(); - var plans = root.setArray("plans"); - for (var plan : planRegistry.all()) { - var p = plans.addObject(); - p.setString("id", plan.id().value()); - p.setString("name", plan.displayName()); - } - return new SlimeJsonResponse(slime); - } - - private HttpResponse getLineItems(String tenant) { - var slimeResponse = new Slime(); - var root = slimeResponse.setObject(); - var lineItems = root.setArray("lineItems"); - - billingController.getUnusedLineItems(TenantName.from(tenant)) - .forEach(lineItem -> { - var itemCursor = lineItems.addObject(); - renderLineItemToCursor(itemCursor, lineItem); - }); - - return new SlimeJsonResponse(slimeResponse); - } - - private void getPlanForTenant(Cursor cursor, TenantName tenant) { - PlanId plan = billingController.getPlan(tenant); - cursor.setString("plan", plan.value()); - cursor.setString("planName", billingController.getPlanDisplayName(plan)); - } - - private void renderInstrument(Cursor cursor, PaymentInstrument instrument) { - cursor.setString("pi-id", instrument.getId()); - cursor.setString("type", instrument.getType()); - cursor.setString("brand", instrument.getBrand()); - cursor.setString("endingWith", instrument.getEndingWith()); - cursor.setString("expiryDate", instrument.getExpiryDate()); - cursor.setString("displayText", instrument.getDisplayText()); - cursor.setString("nameOnCard", instrument.getNameOnCard()); - cursor.setString("addressLine1", instrument.getAddressLine1()); - cursor.setString("addressLine2", instrument.getAddressLine2()); - cursor.setString("zip", instrument.getZip()); - cursor.setString("city", instrument.getCity()); - cursor.setString("state", instrument.getState()); - cursor.setString("country", instrument.getCountry()); - - } - - private void renderCurrentUsage(Cursor cursor, Bill currentUsage) { - if (currentUsage == null) return; - cursor.setString("amount", currentUsage.sum().toPlainString()); - cursor.setString("status", "accrued"); - cursor.setString("from", currentUsage.getStartDate().format(DATE_TIME_FORMATTER)); - var itemsCursor = cursor.setArray("items"); - currentUsage.lineItems().forEach(lineItem -> { - var itemCursor = itemsCursor.addObject(); - renderLineItemToCursor(itemCursor, lineItem); - }); - } - - private void renderAdditionalItems(Cursor cursor, List<Bill.LineItem> items) { - items.forEach(item -> { - renderLineItemToCursor(cursor.addObject(), item); - }); - } - - private Bill getCurrentUsageForTenant(TenantName tenant, LocalDate until) { - return billingController.createUncommittedBill(tenant, until); - } - - private List<Bill> getBillsForTenant(TenantName tenant) { - return billingController.getBillsForTenant(tenant); - } - - private void renderBills(Cursor cursor, List<Bill> bills) { - bills.forEach(bill -> { - var billCursor = cursor.addObject(); - renderBillToCursor(billCursor, bill); - }); - } - - private void renderBillToCursor(Cursor billCursor, Bill bill) { - billCursor.setString("id", bill.id().value()); - billCursor.setString("from", bill.getStartDate().format(DATE_TIME_FORMATTER)); - billCursor.setString("to", bill.getEndDate().format(DATE_TIME_FORMATTER)); - - billCursor.setString("amount", bill.sum().toString()); - billCursor.setString("status", bill.status().value()); - var statusCursor = billCursor.setArray("statusHistory"); - renderStatusHistory(statusCursor, bill.statusHistory()); - - - var lineItemsCursor = billCursor.setArray("items"); - bill.lineItems().forEach(lineItem -> { - var itemCursor = lineItemsCursor.addObject(); - renderLineItemToCursor(itemCursor, lineItem); - }); - } - - private void renderStatusHistory(Cursor cursor, StatusHistory statusHistory) { - statusHistory.getHistory() - .entrySet() - .stream() - .forEach(entry -> { - var c = cursor.addObject(); - c.setString("at", entry.getKey().format(DATE_TIME_FORMATTER)); - c.setString("status", entry.getValue().value()); - }); - } - - private void renderLineItemToCursor(Cursor cursor, Bill.LineItem lineItem) { - cursor.setString("id", lineItem.id()); - cursor.setString("description", lineItem.description()); - cursor.setString("amount", lineItem.amount().toString()); - cursor.setString("plan", lineItem.plan()); - cursor.setString("planName", billingController.getPlanDisplayName(PlanId.from(lineItem.plan()))); - - lineItem.applicationId().ifPresent(appId -> { - cursor.setString("application", appId.application().value()); - cursor.setString("instance", appId.instance().value()); - }); - lineItem.zoneId().ifPresent(zoneId -> - cursor.setString("zone", zoneId.value()) - ); - - lineItem.getArchitecture().ifPresent(architecture -> { - cursor.setString("architecture", architecture.name()); - }); - - cursor.setLong("majorVersion", lineItem.getMajorVersion()); - - if (! lineItem.getCloudAccount().isUnspecified()) - cursor.setString("cloudAccount", lineItem.getCloudAccount().value()); - - lineItem.getCpuHours().ifPresent(cpuHours -> - cursor.setString("cpuHours", cpuHours.toString()) - ); - lineItem.getMemoryHours().ifPresent(memoryHours -> - cursor.setString("memoryHours", memoryHours.toString()) - ); - lineItem.getDiskHours().ifPresent(diskHours -> - cursor.setString("diskHours", diskHours.toString()) - ); - lineItem.getGpuHours().ifPresent(gpuHours -> - cursor.setString("gpuHours", gpuHours.toString()) - ); - lineItem.getCpuCost().ifPresent(cpuCost -> - cursor.setString("cpuCost", cpuCost.toString()) - ); - lineItem.getMemoryCost().ifPresent(memoryCost -> - cursor.setString("memoryCost", memoryCost.toString()) - ); - lineItem.getDiskCost().ifPresent(diskCost -> - cursor.setString("diskCost", diskCost.toString()) - ); - lineItem.getGpuCost().ifPresent(gpuCost -> - cursor.setString("gpuCost", gpuCost.toString()) - ); - } - - private HttpResponse deleteInstrument(String tenant, String userId, String instrument) { - if (billingController.deleteInstrument(TenantName.from(tenant), userId, instrument)) { - return new StringResponse("OK"); - } else { - return ErrorResponse.forbidden("Cannot delete payment instrument you don't own"); - } - } - - private HttpResponse deleteLineItem(String lineItemId) { - billingController.deleteLineItem(lineItemId); - return new MessageResponse("Succesfully deleted line item " + lineItemId); - } - - private HttpResponse patchActiveInstrument(HttpRequest request, String tenant, String userId) { - var inspector = inspectorOrThrow(request); - String instrumentId = getInspectorFieldOrThrow(inspector, "active"); - InstrumentOwner paymentInstrument = new InstrumentOwner(TenantName.from(tenant), userId, instrumentId, true); - boolean success = billingController.setActivePaymentInstrument(paymentInstrument); - return success ? new StringResponse("OK") : ErrorResponse.internalServerError("Failed to patch active instrument"); - } - - private Inspector inspectorOrThrow(HttpRequest request) { - try { - return SlimeUtils.jsonToSlime(request.getData().readAllBytes()).get(); - } catch (IOException e) { - throw new IllegalArgumentException("Failed to parse request body"); - } - } - - private static String getInspectorFieldOrThrow(Inspector inspector, String field) { - if (!inspector.field(field).valid()) - throw new IllegalArgumentException("Field " + field + " cannot be null"); - return inspector.field(field).asString(); - } - - private LocalDate untilParameter(String until) { - if (until == null || until.isEmpty() || until.isBlank()) - return LocalDate.now(); - return LocalDate.parse(until); - } - - private boolean hasDeployments(TenantName tenantName) { - return applicationController.asList(tenantName) - .stream() - .flatMap(app -> app.instances().values() - .stream() - .flatMap(instance -> instance.deployments().values().stream()) - ) - .count() > 0; - } - - private static Set<Role> requestRoles(HttpRequest request) { - return Optional.ofNullable(request.getJDiscRequest().context().get(SecurityContext.ATTRIBUTE_NAME)) - .filter(SecurityContext.class::isInstance) - .map(SecurityContext.class::cast) - .map(SecurityContext::roles) - .orElseThrow(() -> new IllegalArgumentException("Attribute '" + SecurityContext.ATTRIBUTE_NAME + "' was not set on request")); - } - -} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java deleted file mode 100644 index f3147d2adde..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.controller.restapi.billing; - -import com.yahoo.config.provision.SystemName; -import com.yahoo.config.provision.TenantName; -import com.yahoo.vespa.hosted.controller.api.integration.billing.Bill; -import com.yahoo.vespa.hosted.controller.api.integration.billing.BillStatus; -import com.yahoo.vespa.hosted.controller.api.integration.billing.CollectionMethod; -import com.yahoo.vespa.hosted.controller.api.integration.billing.MockBillingController; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; -import com.yahoo.vespa.hosted.controller.api.integration.billing.StatusHistory; -import com.yahoo.vespa.hosted.controller.api.role.Role; -import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; -import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest; -import com.yahoo.vespa.hosted.controller.security.Auth0Credentials; -import com.yahoo.vespa.hosted.controller.security.CloudTenantSpec; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.TreeMap; - -import static com.yahoo.application.container.handler.Request.Method.GET; -import static com.yahoo.application.container.handler.Request.Method.PATCH; -import static com.yahoo.application.container.handler.Request.Method.POST; -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * @author olaa - */ -public class BillingApiHandlerTest extends ControllerContainerCloudTest { - - private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/"; - private static final TenantName tenant = TenantName.from("tenant1"); - private static final TenantName tenant2 = TenantName.from("tenant2"); - private static final Set<Role> tenantRole = Set.of(Role.administrator(tenant)); - private static final Set<Role> financeAdmin = Set.of(Role.hostedAccountant()); - private MockBillingController billingController; - - private ContainerTester tester; - - @BeforeEach - public void setup() { - tester = new ContainerTester(container, responseFiles); - billingController = (MockBillingController) tester.serviceRegistry().billingController(); - } - - @Override - protected SystemName system() { - return SystemName.PublicCd; - } - - @Override - protected String variablePartXml() { - return " <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControlRequests'/>\n" + - " <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControl'/>\n" + - - " <handler id='com.yahoo.vespa.hosted.controller.restapi.billing.BillingApiHandler'>\n" + - " <binding>http://*/billing/v1/*</binding>\n" + - " </handler>\n" + - - " <http>\n" + - " <server id='default' port='8080' />\n" + - " <filtering>\n" + - " <request-chain id='default'>\n" + - " <filter id='com.yahoo.vespa.hosted.controller.restapi.filter.ControllerAuthorizationFilter'/>\n" + - " <binding>http://*/*</binding>\n" + - " </request-chain>\n" + - " </filtering>\n" + - " </http>\n"; - } - - @Test - void list_plans() { - var listPlansRequest = request("/billing/v1/plans", GET) - .roles(Role.hostedAccountant()); - tester.assertResponse(listPlansRequest, "{\"plans\":[{\"id\":\"trial\",\"name\":\"Free Trial - for testing purposes\"},{\"id\":\"paid\",\"name\":\"Paid Plan - for testing purposes\"},{\"id\":\"none\",\"name\":\"None Plan - for testing purposes\"}]}"); - } - - @Test - void response_list_bills() { - var bill = createBill(); - - billingController.addBill(tenant, bill, true); - billingController.addBill(tenant, bill, false); - billingController.setPlan(tenant, PlanId.from("some-plan"), true, false); - - var request = request("/billing/v1/tenant/tenant1/billing?until=2020-05-28").roles(tenantRole); - tester.assertResponse(request, new File("tenant-billing-view.json")); - - } - - @Test - void test_bill_creation() { - var bills = billingController.getBillsForTenant(tenant); - assertEquals(0, bills.size()); - - String requestBody = "{\"tenant\":\"tenant1\", \"startTime\":\"2020-04-20\", \"endTime\":\"2020-05-20\"}"; - var request = request("/billing/v1/invoice", POST) - .data(requestBody) - .roles(tenantRole); - - tester.assertResponse(request, accessDenied, 403); - request.roles(financeAdmin); - tester.assertResponse(request, new File("invoice-creation-response.json")); - - bills = billingController.getBillsForTenant(tenant); - assertEquals(1, bills.size()); - Bill bill = bills.get(0); - assertEquals("2020-04-20T00:00Z", bill.getStartTime().toString()); - assertEquals("2020-05-21T00:00Z", bill.getEndTime().toString()); - - assertEquals("2020-04-20", bill.getStartDate().toString()); - assertEquals("2020-05-20", bill.getEndDate().toString()); - } - - @Test - void adding_and_listing_line_item() { - - var requestBody = "{" + - "\"description\":\"some description\"," + - "\"amount\":\"123.45\" " + - "}"; - - var request = request("/billing/v1/invoice/tenant/tenant1/line-item", POST) - .data(requestBody) - .roles(financeAdmin); - - tester.assertResponse(request, "{\"message\":\"Added line item for tenant tenant1\"}"); - - var lineItems = billingController.getUnusedLineItems(tenant); - assertEquals(1, lineItems.size()); - Bill.LineItem lineItem = lineItems.get(0); - assertEquals("some description", lineItem.description()); - assertEquals(new BigDecimal("123.45"), lineItem.amount()); - - request = request("/billing/v1/invoice/tenant/tenant1/line-item") - .roles(financeAdmin); - - tester.assertResponse(request, new File("line-item-list.json")); - } - - @Test - void adding_new_status() { - billingController.addBill(tenant, createBill(), true); - - var requestBody = "{\"status\":\"CLOSED\"}"; - var request = request("/billing/v1/invoice/id-1/status", POST) - .data(requestBody) - .roles(financeAdmin); - tester.assertResponse(request, "{\"message\":\"Updated status of invoice id-1\"}"); - - var bill = billingController.getBillsForTenant(tenant).get(0); - assertEquals(BillStatus.CLOSED, bill.status()); - } - - @Test - void list_all_unbilled_items() { - tester.controller().tenants().create(new CloudTenantSpec(tenant, ""), new Auth0Credentials(() -> "foo", Set.of(Role.hostedOperator()))); - tester.controller().tenants().create(new CloudTenantSpec(tenant2, ""), new Auth0Credentials(() -> "foo", Set.of(Role.hostedOperator()))); - - var bill = createBill(); - billingController.setPlan(tenant, PlanId.from("some-plan"), true, false); - billingController.setPlan(tenant2, PlanId.from("some-plan"), true, false); - billingController.addBill(tenant, bill, false); - billingController.addLineItem(tenant, "support", new BigDecimal("42"), Optional.empty(), "Smith"); - billingController.addBill(tenant2, bill, false); - - var request = request("/billing/v1/billing?until=2020-05-28").roles(financeAdmin); - - tester.assertResponse(request, new File("billing-all-tenants.json")); - } - - @Test - void csv_export() { - var bill = createBill(); - billingController.addBill(tenant, bill, true); - var csvRequest = request("/billing/v1/invoice/export", GET).roles(financeAdmin); - tester.assertResponse(csvRequest.get(), new File("billing-all-invoices"), 200, false); - } - - @Test - void patch_collection_method() { - test_patch_collection_with_field_name("collectionMethod"); - test_patch_collection_with_field_name("collection"); - } - - private void test_patch_collection_with_field_name(String fieldName) { - var planRequest = request("/billing/v1/tenant/tenant1/collection", PATCH) - .data("{\"" + fieldName + "\": \"invoice\"}") - .roles(financeAdmin); - tester.assertResponse(planRequest, "Collection method updated to INVOICE"); - assertEquals(CollectionMethod.INVOICE, billingController.getCollectionMethod(tenant)); - - // Test that not event tenant administrators can do this - planRequest = request("/billing/v1/tenant/tenant1/collection", PATCH) - .data("{\"collectionMethod\": \"epay\"}") - .roles(tenantRole); - tester.assertResponse(planRequest, accessDenied, 403); - assertEquals(CollectionMethod.INVOICE, billingController.getCollectionMethod(tenant)); - } - - static Bill createBill() { - var start = LocalDate.of(2020, 5, 23).atStartOfDay(ZoneOffset.UTC); - var end = start.toLocalDate().plusDays(6).atStartOfDay(ZoneOffset.UTC); - var statusHistory = new StatusHistory(new TreeMap<>(Map.of(start, BillStatus.OPEN))); - return new Bill( - Bill.Id.of("id-1"), - TenantName.defaultName(), - statusHistory, - List.of(createLineItem(start)), - start, - end - ); - } - - static Bill.LineItem createLineItem(ZonedDateTime addedAt) { - return new Bill.LineItem( - "some-id", - "description", - new BigDecimal("123.00"), - "paid", - "Smith", - addedAt - ); - } - -} |