aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src
diff options
context:
space:
mode:
authorØyvind Grønnesby <oyving@verizonmedia.com>2021-05-04 15:42:02 +0200
committerØyvind Grønnesby <oyving@verizonmedia.com>2021-05-04 15:42:02 +0200
commitde02247aea4521aa923b2e62284d5200dd4fe57f (patch)
tree02a30e68d3e55f8fa609d054357424a889a9c024 /controller-server/src
parent0d1ae535057f3884fc4c438a5886c2af85347781 (diff)
Accountant methods for BillingApiHandlerV2
- Create the accountant methods to preview and create bills - Make sure classes take a Clock through constructor to help with testing - Update path groups for accountant view
Diffstat (limited to 'controller-server/src')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java92
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java26
2 files changed, 93 insertions, 25 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java
index 2ad0dece8d9..bfcefecba0c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java
@@ -1,14 +1,15 @@
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.LoggingRequestHandler;
+import com.yahoo.restapi.MessageResponse;
import com.yahoo.restapi.RestApi;
import com.yahoo.restapi.RestApiException;
import com.yahoo.restapi.RestApiRequestHandler;
import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.slime.Type;
@@ -22,18 +23,19 @@ import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
+import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import javax.ws.rs.ForbiddenException;
+import javax.ws.rs.BadRequestException;
import java.io.IOException;
import java.math.BigDecimal;
-import java.security.Principal;
import java.time.Clock;
+import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
+import java.util.Comparator;
import java.util.Optional;
import java.util.List;
-import java.util.function.Function;
/**
* @author ogronnesby
@@ -73,6 +75,9 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
*/
.addRoute(RestApi.route("/billing/v2/accountant")
.get(self::accountant))
+ .addRoute(RestApi.route("/billing/v2/accountant/preview/tenant/{tenant}")
+ .get(self::previewBill)
+ .post(Slime.class, self::createBill))
/*
* Utility - map Slime.class => SlimeJsonResponse
*/
@@ -179,10 +184,7 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
private Slime tenantUsage(RestApi.RequestContext requestContext) {
var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
var tenant = tenants.require(tenantName, CloudTenant.class);
- var untilAt = requestContext.queryParameters().getString("until")
- .map(LocalDate::parse)
- .map(date -> date.plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant())
- .orElseGet(clock::instant);
+ var untilAt = untilParameter(requestContext);
var usage = billing.createUncommittedInvoice(tenant.name(), untilAt.atZone(ZoneOffset.UTC).toLocalDate());
var slime = new Slime();
@@ -193,7 +195,52 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
// --------- ACCOUNTANT API ----------
private Slime accountant(RestApi.RequestContext requestContext) {
- return null;
+ var untilAt = untilParameter(requestContext);
+ var usagePerTenant = billing.createUncommittedInvoices(untilAt.atZone(ZoneOffset.UTC).toLocalDate());
+
+ var response = new Slime();
+ var tenantsResponse = response.setObject().setArray("tenants");
+ tenants.asList().stream().sorted(Comparator.comparing(Tenant::name)).forEach(tenant -> {
+ var usage = Optional.ofNullable(usagePerTenant.get(tenant.name()));
+ var tenantResponse = tenantsResponse.addObject();
+ tenantResponse.setString("tenant", tenant.name().value());
+ tenantResponse.setString("plan", billing.getPlan(tenant.name()).value());
+ tenantResponse.setString("collection", billing.getCollectionMethod(tenant.name()).name());
+ tenantResponse.setString("lastBill", usage.map(Invoice::getStartTime).map(DateTimeFormatter.ISO_DATE::format).orElse(null));
+ tenantResponse.setString("unbilled", usage.map(Invoice::sum).map(BigDecimal::toPlainString).orElse("0.00"));
+ });
+
+ return response;
+ }
+
+ private Slime previewBill(RestApi.RequestContext requestContext) {
+ var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
+ var tenant = tenants.require(tenantName, CloudTenant.class);
+ var untilAt = untilParameter(requestContext);
+
+ var usage = billing.createUncommittedInvoice(tenant.name(), untilAt.atZone(ZoneOffset.UTC).toLocalDate());
+
+ var slime = new Slime();
+ toSlime(slime.setObject(), usage);
+ return slime;
+ }
+
+ private HttpResponse createBill(RestApi.RequestContext requestContext, Slime slime) {
+ var body = slime.get();
+ var security = requestContext.attributes().get(SecurityContext.ATTRIBUTE_NAME)
+ .map(SecurityContext.class::cast)
+ .orElseThrow(() -> new RestApiException.Forbidden("Must be logged in"));
+
+ var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
+ var tenant = tenants.require(tenantName, CloudTenant.class);
+
+ var startAt = LocalDate.parse(getInspectorFieldOrThrow(body, "from")).atStartOfDay(ZoneOffset.UTC);
+ var endAt = LocalDate.parse(getInspectorFieldOrThrow(body, "to")).atStartOfDay(ZoneOffset.UTC);
+
+ var invoiceId = billing.createInvoiceForPeriod(tenant.name(), startAt, endAt, security.principal().getName());
+
+ // TODO: Make a redirect to the bill itself
+ return new MessageResponse("Created bill " + invoiceId.value());
}
@@ -277,10 +324,17 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
// ---------- END INVOICE RENDERING ----------
- private static String userIdOrThrow(HttpRequest request) {
- return Optional.ofNullable(request.getJDiscRequest().getUserPrincipal())
- .map(Principal::getName)
- .orElseThrow(() -> new ForbiddenException("Must be authenticated to use this API"));
+ private Instant untilParameter(RestApi.RequestContext ctx) {
+ return ctx.queryParameters().getString("until")
+ .map(LocalDate::parse)
+ .map(date -> date.plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant())
+ .orElseGet(clock::instant);
+ }
+
+ private static String getInspectorFieldOrThrow(Inspector inspector, String field) {
+ if (!inspector.field(field).valid())
+ throw new BadRequestException("Field " + field + " cannot be null");
+ return inspector.field(field).asString();
}
private static Optional<Slime> slimeRequestMapper(RestApi.RequestContext requestContext) {
@@ -291,20 +345,8 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
}
}
- private static <T> RestApi.Handler<T> withCloudTenant(TenantController ctl, FunctionWithCloudTenant<T> f) {
- return (RestApi.RequestContext ctx) -> {
- var tenantName = TenantName.from(ctx.pathParameters().getStringOrThrow("tenant"));
- var tenant = ctl.require(tenantName, CloudTenant.class);
- return f.applyWithTenant(ctx, tenant);
- };
- }
-
private static HttpResponse slimeResponseMapper(RestApi.RequestContext ctx, Slime slime) {
return new SlimeJsonResponse(slime);
}
- @FunctionalInterface
- private interface FunctionWithCloudTenant<T> {
- T applyWithTenant(RestApi.RequestContext ctx, CloudTenant tenant);
- }
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java
index 8122c4c9679..43754261001 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java
@@ -109,4 +109,30 @@ public class BillingApiHandlerV2Test extends ControllerContainerCloudTest {
var singleRequest = request("/billing/v2/tenant/" + tenant + "/bill/id-1").roles(tenantReader);
tester.assertResponse(singleRequest, "{\"id\":\"id-1\",\"from\":\"2020-05-23\",\"to\":\"2020-05-23\",\"total\":\"123.00\",\"status\":\"OPEN\",\"statusHistory\":[{\"at\":\"2020-05-23T00:00:00+02:00\",\"status\":\"OPEN\"}],\"items\":[{\"id\":\"some-id\",\"description\":\"description\",\"amount\":\"123.00\",\"plan\":\"some-plan\",\"planName\":\"Plan with id: some-plan\",\"cpu\":{},\"memory\":{},\"disk\":{}}]}");
}
+
+ @Test
+ public void require_accountant_summary() {
+ var tenantRequest = request("/billing/v2/accountant").roles(tenantReader);
+ tester.assertResponse(tenantRequest, "{\n" +
+ " \"code\" : 403,\n" +
+ " \"message\" : \"Access denied\"\n" +
+ "}", 403);
+
+ var accountantRequest = request("/billing/v2/accountant").roles(Role.hostedAccountant());
+ tester.assertResponse(accountantRequest, "{\"tenants\":[{\"tenant\":\"tenant1\",\"plan\":\"trial\",\"collection\":\"AUTO\",\"lastBill\":null,\"unbilled\":\"0.00\"}]}");
+ }
+
+ @Test
+ public void require_accountant_tenant_preview() {
+ var accountantRequest = request("/billing/v2/accountant/preview/tenant/tenant1").roles(Role.hostedAccountant());
+ tester.assertResponse(accountantRequest, "{\"id\":\"empty\",\"from\":\"2021-04-13\",\"to\":\"2021-04-13\",\"total\":\"0.00\",\"status\":\"OPEN\",\"statusHistory\":[{\"at\":\"2021-04-13T00:00:00Z\",\"status\":\"OPEN\"}],\"items\":[]}");
+ }
+
+ @Test
+ public void require_accountant_tenant_bill() {
+ var accountantRequest = request("/billing/v2/accountant/preview/tenant/tenant1", Request.Method.POST)
+ .roles(Role.hostedAccountant())
+ .data("{\"from\": \"2020-05-01\",\"to\": \"2020-06-01\"}");
+ tester.assertResponse(accountantRequest, "{\"message\":\"Created bill id-123\"}");
+ }
}