summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorØyvind Grønnesby <oyving@verizonmedia.com>2021-10-11 10:36:47 +0200
committerGitHub <noreply@github.com>2021-10-11 10:36:47 +0200
commitb96801ba355506890fbd16d7b1270d09746911ef (patch)
tree727b2160b9062d1f55ed81419622f04d51ceeed1
parentb797f6b03640ff130b2949f23ef731df464c6151 (diff)
parent5e9f649da988f171e34952f6626096dbd4c2093b (diff)
Merge pull request #19496 from vespa-engine/revert-19485-revert-18572-ogronnesby/billing-service
Revert "Revert "Billing refactoring and cleanup""
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java9
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java (renamed from controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Invoice.java)18
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java71
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java135
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java178
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java60
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java23
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java122
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java51
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java146
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java94
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java80
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java24
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java57
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java2
16 files changed, 911 insertions, 163 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java
index 4714b74ba94..93e3f5585c8 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java
@@ -7,6 +7,8 @@ import com.yahoo.vespa.hosted.controller.api.integration.aws.RoleService;
import com.yahoo.vespa.hosted.controller.api.integration.aws.CloudEventFetcher;
import com.yahoo.vespa.hosted.controller.api.integration.aws.ResourceTagger;
import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingDatabaseClient;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidator;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
@@ -25,6 +27,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipI
import com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor;
import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumer;
import com.yahoo.vespa.hosted.controller.api.integration.resource.MeteringClient;
+import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClient;
import com.yahoo.vespa.hosted.controller.api.integration.routing.GlobalRoutingService;
import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretService;
import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestClient;
@@ -88,6 +91,10 @@ public interface ServiceRegistry {
BillingController billingController();
+ ResourceDatabaseClient resourceDatabase();
+
+ BillingDatabaseClient billingDatabase();
+
ContainerRegistry containerRegistry();
TenantSecretService tenantSecretService();
@@ -99,4 +106,6 @@ public interface ServiceRegistry {
AccessControlService accessControlService();
HorizonClient horizonClient();
+
+ PlanRegistry planRegistry();
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Invoice.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java
index 3789021ae8e..d1af5b428de 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Invoice.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java
@@ -20,18 +20,18 @@ import java.util.function.Function;
/**
- * An Invoice is an identifier with a status (with history) and line items. A line item is the meat and
- * potatoes of the content of the invoice, and are a history of items. Most line items are connected to
+ * An Bill is an identifier with a status (with history) and line items. A line item is the meat and
+ * potatoes of the content of the bill, and are a history of items. Most line items are connected to
* a given deployment in Vespa Cloud, but they can also be manually added to e.g. give a discount or represent
* support.
* <p>
* All line items have a Plan associated with them - which was used to map from utilization to an actual price.
* <p>
- * The invoice has a status history, but only the latest status is exposed through this API.
+ * The bill has a status history, but only the latest status is exposed through this API.
*
* @author ogronnesby
*/
-public class Invoice {
+public class Bill {
private static final BigDecimal SCALED_ZERO = new BigDecimal("0.00");
private final Id id;
@@ -41,7 +41,7 @@ public class Invoice {
private final ZonedDateTime startTime;
private final ZonedDateTime endTime;
- public Invoice(Id id, TenantName tenant, StatusHistory statusHistory, List<LineItem> lineItems, ZonedDateTime startTime, ZonedDateTime endTime) {
+ public Bill(Id id, TenantName tenant, StatusHistory statusHistory, List<LineItem> lineItems, ZonedDateTime startTime, ZonedDateTime endTime) {
this.id = id;
this.tenant = tenant;
this.lineItems = List.copyOf(lineItems);
@@ -141,8 +141,8 @@ public class Invoice {
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- Id invoiceId = (Id) o;
- return value.equals(invoiceId.value);
+ Id billId = (Id) o;
+ return value.equals(billId.value);
}
@Override
@@ -152,14 +152,14 @@ public class Invoice {
@Override
public String toString() {
- return "InvoiceId{" +
+ return "BillId{" +
"value='" + value + '\'' +
'}';
}
}
/**
- * Represents a chargeable line on an invoice.
+ * Represents a chargeable line on a bill.
*/
public static class LineItem {
private final String id;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java
index 91916975146..61f8844482c 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java
@@ -12,55 +12,112 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
+/**
+ * A service that controls creation of bills based on the resource usage of a tenant, controls the quota for a
+ * tenant, and controls the plan the tenant is on.
+ *
+ * @author ogronnesby
+ * @author olaa
+ */
public interface BillingController {
+ /**
+ * Get the plan ID for the given tenant.
+ * This method will not fail if the tenant does not exist, it will return the default plan for that tenant instead.
+ */
PlanId getPlan(TenantName tenant);
+ /**
+ * Return the list of tenants with the given plan.
+ * @param existing All existing tenants in the system
+ * @param planId The ID of the plan to filter existing tenants on.
+ * @return The tenants that have the given plan.
+ */
List<TenantName> tenantsWithPlan(List<TenantName> existing, PlanId planId);
+ /** The display name of the given plan */
String getPlanDisplayName(PlanId planId);
+ /**
+ * The quota for the given tenant.
+ * This method will return default quota for tenants that do not exist.
+ */
Quota getQuota(TenantName tenant);
/**
+ * Set the plan for the current tenant. Checks some pre-conditions to see if the tenant is eligible for the
+ * given plan.
+ * @param tenant The name of the tenant.
+ * @param planId The ID of the plan to change to.
+ * @param hasDeployments Does the tenant have active deployments.
* @return String containing error message if something went wrong. Empty otherwise
*/
PlanResult setPlan(TenantName tenant, PlanId planId, boolean hasDeployments);
- Invoice.Id createInvoiceForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent);
+ /**
+ * Create a bill of unbilled use for the given tenant in the given time period.
+ * @param tenant The name of the tenant.
+ * @param startTime The start of the billing period
+ * @param endTime The end of the billing period
+ * @param agent The agent that creates the bill
+ * @return The ID of the new bill.
+ */
+ Bill.Id createBillForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent);
- Invoice createUncommittedInvoice(TenantName tenant, LocalDate until);
+ /**
+ * Create an unpersisted bill of unbilled use for the given tenant from the end of last bill until the given date.
+ * This is used to show "unbilled use" in the Console.
+ * @param tenant The name of the tenant.
+ * @param until The end date of the unbilled use period.
+ * @return A bill with the resource use and cost.
+ */
+ Bill createUncommittedBill(TenantName tenant, LocalDate until);
- Map<TenantName, Invoice> createUncommittedInvoices(LocalDate until);
+ /** Run {createUncommittedBill} for all tenants with unbilled use */
+ Map<TenantName, Bill> createUncommittedBills(LocalDate until);
- List<Invoice.LineItem> getUnusedLineItems(TenantName tenant);
+ /** Get line items that have been manually added to a tenant, but is not yet part of a bill */
+ List<Bill.LineItem> getUnusedLineItems(TenantName tenant);
+ /** Get the payment instrument for the given tenant */
Optional<PaymentInstrument> getDefaultInstrument(TenantName tenant);
+ /** Get the auth token needed to talk to payment services */
String createClientToken(String tenant, String userId);
+ /** Delete a payment instrument from the list of the tenant's instruments */
boolean deleteInstrument(TenantName tenant, String userId, String instrumentId);
- void updateInvoiceStatus(Invoice.Id invoiceId, String agent, String status);
+ /** Change the status of the given bill */
+ void updateBillStatus(Bill.Id billId, String agent, String status);
+ /** Add a line item to the given bill */
void addLineItem(TenantName tenant, String description, BigDecimal amount, String agent);
+ /** Delete a line item - only available for unused line items */
void deleteLineItem(String lineItemId);
+ /** Set the given payment instrument as the active instrument for the tenant */
boolean setActivePaymentInstrument(InstrumentOwner paymentInstrument);
+ /** List the payment instruments from the tenant */
InstrumentList listInstruments(TenantName tenant, String userId);
- List<Invoice> getInvoicesForTenant(TenantName tenant);
+ /** Get all bills for the given tenant */
+ List<Bill> getBillsForTenant(TenantName tenant);
- List<Invoice> getInvoices();
+ /** Get all bills from the system */
+ List<Bill> getBills();
+ /** Delete billing contact information from the tenant */
void deleteBillingInfo(TenantName tenant, Set<User> users, boolean isPrivileged);
+ /** Get the bill collection method for the given tenant */
default CollectionMethod getCollectionMethod(TenantName tenant) {
return CollectionMethod.NONE;
}
+ /** Set the bill collection method for the given tenant */
default CollectionResult setCollectionMethod(TenantName tenant, CollectionMethod method) {
return CollectionResult.error("Method not implemented");
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java
new file mode 100644
index 00000000000..4891fe0ffa7
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java
@@ -0,0 +1,135 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.api.integration.billing;
+
+import com.yahoo.config.provision.TenantName;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Interface that talks about bills in the billing API. It is a layer on top of the SQL
+ * database where we store data about bills.
+ *
+ * @author olaa
+ * @author ogronnesby
+ */
+public interface BillingDatabaseClient {
+
+ boolean setActivePaymentInstrument(InstrumentOwner paymentInstrument);
+
+ Optional<InstrumentOwner> getDefaultPaymentInstrumentForTenant(TenantName from);
+
+ /**
+ * Create a completely new Bill in the open state with no LineItems.
+ *
+ * @param tenant The name of the tenant the bill is for
+ * @param agent The agent that created the bill
+ * @return The Id of the new bill
+ */
+ Bill.Id createBill(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent);
+
+ /**
+ * Read the given bill from the data source
+ *
+ * @param billId The Id of the bill to retrieve
+ * @return The Bill if it exists, Optional.empty() if not.
+ */
+ Optional<Bill> readBill(Bill.Id billId);
+
+ /**
+ * Get all bills for a given tenant, ordered by date
+ *
+ * @param tenant The name of the tenant
+ * @return List of all bills ordered by date
+ */
+ List<Bill> readBillsForTenant(TenantName tenant);
+
+ /**
+ * Read all bills, ordered by date
+ * @return List of all bills ordered by date
+ */
+ List<Bill> readBills();
+
+ /**
+ * Add a line item to an open bill
+ *
+ * @param lineItem
+ * @param billId The optional ID of the bill this line item is for
+ * @return The Id of the new line item
+ * @throws RuntimeException if the bill is not in OPEN state
+ */
+ String addLineItem(TenantName tenantName, Bill.LineItem lineItem, Optional<Bill.Id> billId);
+
+ /**
+ * Set status for the given bill
+ *
+ * @param billId The ID of the bill this status is for
+ * @param agent The agent that added the status
+ * @param status The new status of the bill
+ */
+ void setStatus(Bill.Id billId, String agent, String status);
+
+ List<Bill.LineItem> getUnusedLineItems(TenantName tenantName);
+
+ /**
+ * Delete a line item
+ * This is only allowed if the line item has not yet been associated with an bill
+ *
+ * @param lineItemId The ID of the line item
+ * @throws RuntimeException if the line item is associated with an bill
+ */
+ void deleteLineItem(String lineItemId);
+
+ /**
+ * Associate all uncommitted line items to a given bill
+ * This is only allowed if the line item has not already been associated with an bill
+ *
+ * @param tenantName The tenant we want to commit line items for
+ * @param billId The ID of the line item
+ * @throws RuntimeException if the line item is already associated with an bill
+ */
+ void commitLineItems(TenantName tenantName, Bill.Id billId);
+
+ /**
+ * Return the plan for the given tenant
+ *
+ * @param tenantName The tenant to retrieve the plan for
+ * @return Optional.of the plan if present in DN, else Optional.empty
+ */
+ Optional<Plan> getPlan(TenantName tenantName);
+
+ /**
+ * Return the plan for the given tenants if present.
+ * If the database does not know of the tenant, the tenant is not included in the result.
+ */
+ Map<TenantName, Optional<Plan>> getPlans(List<TenantName> tenants);
+
+ /**
+ * Set the current plan for the given tenant
+ *
+ * @param tenantName The tenant to set the plan for
+ * @param plan The plan to use
+ */
+ void setPlan(TenantName tenantName, Plan plan);
+
+ /**
+ * Deactivates the default payment instrument for a tenant, if it exists.
+ * Used during tenant deletion
+ */
+ void deactivateDefaultPaymentInstrument(TenantName tenant);
+
+ /**
+ * Get the current collection method for the tenant - if one has persisted
+ * @return Optional.empty if no collection method has been persisted for the tenant
+ */
+ Optional<CollectionMethod> getCollectionMethod(TenantName tenantName);
+
+ /**
+ * Set the collection method for the tenant
+ * @param tenantName The name of the tenant to set collection method for
+ * @param collectionMethod The collection method for the tenant
+ */
+ void setCollectionMethod(TenantName tenantName, CollectionMethod collectionMethod);
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java
new file mode 100644
index 00000000000..f53025a2e6d
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java
@@ -0,0 +1,178 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.api.integration.billing;
+
+import com.yahoo.config.provision.TenantName;
+
+import java.time.Clock;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * @author olaa
+ */
+public class BillingDatabaseClientMock implements BillingDatabaseClient {
+ private final Clock clock;
+ private final PlanRegistry planRegistry;
+ private final Map<TenantName, Plan> tenantPlans = new HashMap<>();
+ private final Map<Bill.Id, TenantName> invoices = new HashMap<>();
+ private final Map<Bill.Id, List<Bill.LineItem>> lineItems = new HashMap<>();
+ private final Map<TenantName, List<Bill.LineItem>> uncommittedLineItems = new HashMap<>();
+
+ private final Map<Bill.Id, Bill.StatusHistory> statuses = new HashMap<>();
+ private final Map<Bill.Id, ZonedDateTime> startTimes = new HashMap<>();
+ private final Map<Bill.Id, ZonedDateTime> endTimes = new HashMap<>();
+
+ private final ZonedDateTime startTime = LocalDate.of(2020, 4, 1).atStartOfDay(ZoneId.of("UTC"));
+ private final ZonedDateTime endTime = LocalDate.of(2020, 5, 1).atStartOfDay(ZoneId.of("UTC"));
+
+ private final List<InstrumentOwner> paymentInstruments = new ArrayList<>();
+ private final Map<TenantName, CollectionMethod> collectionMethods = new HashMap<>();
+
+ public BillingDatabaseClientMock(Clock clock, PlanRegistry planRegistry) {
+ this.clock = clock;
+ this.planRegistry = planRegistry;
+ }
+
+ @Override
+ public boolean setActivePaymentInstrument(InstrumentOwner paymentInstrument) {
+ return paymentInstruments.add(paymentInstrument);
+ }
+
+ @Override
+ public Optional<InstrumentOwner> getDefaultPaymentInstrumentForTenant(TenantName tenantName) {
+ return paymentInstruments.stream()
+ .filter(paymentInstrument -> paymentInstrument.getTenantName().equals(tenantName))
+ .findFirst();
+ }
+
+ public String getStatus(Bill.Id invoiceId) {
+ return statuses.get(invoiceId).current();
+ }
+
+ @Override
+ public Bill.Id createBill(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent) {
+ var invoiceId = Bill.Id.generate();
+ invoices.put(invoiceId, tenant);
+ statuses.computeIfAbsent(invoiceId, l -> Bill.StatusHistory.open(clock));
+ startTimes.put(invoiceId, startTime);
+ endTimes.put(invoiceId, endTime);
+ return invoiceId;
+ }
+
+ @Override
+ public Optional<Bill> readBill(Bill.Id billId) {
+ var invoice = Optional.ofNullable(invoices.get(billId));
+ var lines = lineItems.getOrDefault(billId, List.of());
+ var status = statuses.getOrDefault(billId, Bill.StatusHistory.open(clock));
+ var start = startTimes.getOrDefault(billId, startTime);
+ var end = endTimes.getOrDefault(billId, endTime);
+ return invoice.map(tenant -> new Bill(billId, tenant, status, lines, start, end));
+ }
+
+ @Override
+ public String addLineItem(TenantName tenantName, Bill.LineItem lineItem, Optional<Bill.Id> invoiceId) {
+ var lineItemId = UUID.randomUUID().toString();
+ invoiceId.ifPresentOrElse(
+ invoice -> lineItems.computeIfAbsent(invoice, l -> new ArrayList<>()).add(lineItem),
+ () -> uncommittedLineItems.computeIfAbsent(tenantName, l -> new ArrayList<>()).add(lineItem)
+ );
+ return lineItemId;
+ }
+
+ @Override
+ public void setStatus(Bill.Id invoiceId, String agent, String status) {
+ statuses.computeIfAbsent(invoiceId, k -> Bill.StatusHistory.open(clock))
+ .getHistory()
+ .put(ZonedDateTime.now(), status);
+ }
+
+ @Override
+ public List<Bill.LineItem> getUnusedLineItems(TenantName tenantName) {
+ return uncommittedLineItems.getOrDefault(tenantName, new ArrayList<>());
+ }
+
+ @Override
+ public void deleteLineItem(String lineItemId) {
+ uncommittedLineItems.values()
+ .forEach(list ->
+ list.removeIf(lineItem -> lineItem.id().equals(lineItemId))
+ );
+ }
+
+ @Override
+ public void commitLineItems(TenantName tenantName, Bill.Id invoiceId) {
+
+ }
+
+ @Override
+ public Optional<Plan> getPlan(TenantName tenantName) {
+ return Optional.ofNullable(tenantPlans.get(tenantName));
+ }
+
+ @Override
+ public Map<TenantName, Optional<Plan>> getPlans(List<TenantName> tenants) {
+ return tenantPlans.entrySet().stream()
+ .filter(entry -> tenants.contains(entry.getKey()))
+ .collect(Collectors.toMap(
+ entry -> entry.getKey(),
+ entry -> planRegistry.plan(entry.getValue().id())
+ ));
+ }
+
+ @Override
+ public void setPlan(TenantName tenantName, Plan plan) {
+ tenantPlans.put(tenantName, plan);
+ }
+
+ @Override
+ public void deactivateDefaultPaymentInstrument(TenantName tenant) {
+ paymentInstruments.removeIf(instrumentOwner -> instrumentOwner.getTenantName().equals(tenant));
+ }
+
+ @Override
+ public Optional<CollectionMethod> getCollectionMethod(TenantName tenantName) {
+ return Optional.ofNullable(collectionMethods.get(tenantName));
+ }
+
+ @Override
+ public void setCollectionMethod(TenantName tenantName, CollectionMethod collectionMethod) {
+ collectionMethods.put(tenantName, collectionMethod);
+ }
+
+ @Override
+ public List<Bill> readBillsForTenant(TenantName tenant) {
+ return invoices.entrySet().stream()
+ .filter(entry -> entry.getValue().equals(tenant))
+ .map(Map.Entry::getKey)
+ .map(invoiceId -> {
+ var items = lineItems.getOrDefault(invoiceId, List.of());
+ var status = statuses.get(invoiceId);
+ var start = startTimes.get(invoiceId);
+ var end = endTimes.get(invoiceId);
+ return new Bill(invoiceId, tenant, status, items, start, end);
+ })
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List<Bill> readBills() {
+ return invoices.keySet().stream()
+ .map(invoiceId -> {
+ var tenant = invoices.get(invoiceId);
+ var items = lineItems.getOrDefault(invoiceId, List.of());
+ var status = statuses.get(invoiceId);
+ var start = startTimes.get(invoiceId);
+ var end = endTimes.get(invoiceId);
+ return new Bill(invoiceId, tenant, status, items, start, end);
+ })
+ .collect(Collectors.toList());
+ }
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java
index 8816c4eb57b..f4d3577aeec 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java
@@ -26,9 +26,9 @@ public class MockBillingController implements BillingController {
private final Clock clock;
Map<TenantName, PlanId> plans = new HashMap<>();
Map<TenantName, PaymentInstrument> activeInstruments = new HashMap<>();
- Map<TenantName, List<Invoice>> committedInvoices = new HashMap<>();
- Map<TenantName, Invoice> uncommittedInvoices = new HashMap<>();
- Map<TenantName, List<Invoice.LineItem>> unusedLineItems = new HashMap<>();
+ Map<TenantName, List<Bill>> committedBills = new HashMap<>();
+ Map<TenantName, Bill> uncommittedBills = new HashMap<>();
+ Map<TenantName, List<Bill.LineItem>> unusedLineItems = new HashMap<>();
Map<TenantName, CollectionMethod> collectionMethod = new HashMap<>();
public MockBillingController(Clock clock) {
@@ -64,32 +64,32 @@ public class MockBillingController implements BillingController {
}
@Override
- public Invoice.Id createInvoiceForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent) {
- var invoiceId = Invoice.Id.of("id-123");
- committedInvoices.computeIfAbsent(tenant, l -> new ArrayList<>())
- .add(new Invoice(
- invoiceId,
+ public Bill.Id createBillForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent) {
+ var billId = Bill.Id.of("id-123");
+ committedBills.computeIfAbsent(tenant, l -> new ArrayList<>())
+ .add(new Bill(
+ billId,
tenant,
- Invoice.StatusHistory.open(clock),
+ Bill.StatusHistory.open(clock),
List.of(),
startTime,
endTime
));
- return invoiceId;
+ return billId;
}
@Override
- public Invoice createUncommittedInvoice(TenantName tenant, LocalDate until) {
- return uncommittedInvoices.getOrDefault(tenant, emptyInvoice());
+ public Bill createUncommittedBill(TenantName tenant, LocalDate until) {
+ return uncommittedBills.getOrDefault(tenant, emptyBill());
}
@Override
- public Map<TenantName, Invoice> createUncommittedInvoices(LocalDate until) {
- return uncommittedInvoices;
+ public Map<TenantName, Bill> createUncommittedBills(LocalDate until) {
+ return uncommittedBills;
}
@Override
- public List<Invoice.LineItem> getUnusedLineItems(TenantName tenant) {
+ public List<Bill.LineItem> getUnusedLineItems(TenantName tenant) {
return unusedLineItems.getOrDefault(tenant, List.of());
}
@@ -110,18 +110,18 @@ public class MockBillingController implements BillingController {
}
@Override
- public void updateInvoiceStatus(Invoice.Id invoiceId, String agent, String status) {
+ public void updateBillStatus(Bill.Id billId, String agent, String status) {
var now = clock.instant().atZone(ZoneOffset.UTC);
- committedInvoices.values().stream()
+ committedBills.values().stream()
.flatMap(List::stream)
- .filter(invoice -> invoiceId.equals(invoice.id()))
- .forEach(invoice -> invoice.statusHistory().history.put(now, status));
+ .filter(bill -> billId.equals(bill.id()))
+ .forEach(bill -> bill.statusHistory().history.put(now, status));
}
@Override
public void addLineItem(TenantName tenant, String description, BigDecimal amount, String agent) {
unusedLineItems.computeIfAbsent(tenant, l -> new ArrayList<>())
- .add(new Invoice.LineItem(
+ .add(new Bill.LineItem(
"line-item-id",
description,
amount,
@@ -152,13 +152,13 @@ public class MockBillingController implements BillingController {
}
@Override
- public List<Invoice> getInvoicesForTenant(TenantName tenant) {
- return committedInvoices.getOrDefault(tenant, List.of());
+ public List<Bill> getBillsForTenant(TenantName tenant) {
+ return committedBills.getOrDefault(tenant, List.of());
}
@Override
- public List<Invoice> getInvoices() {
- return committedInvoices.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
+ public List<Bill> getBills() {
+ return committedBills.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
}
@Override
@@ -191,17 +191,17 @@ public class MockBillingController implements BillingController {
"country");
}
- public void addInvoice(TenantName tenantName, Invoice invoice, boolean committed) {
+ public void addBill(TenantName tenantName, Bill bill, boolean committed) {
if (committed)
- committedInvoices.computeIfAbsent(tenantName, i -> new ArrayList<>())
- .add(invoice);
+ committedBills.computeIfAbsent(tenantName, i -> new ArrayList<>())
+ .add(bill);
else
- uncommittedInvoices.put(tenantName, invoice);
+ uncommittedBills.put(tenantName, bill);
}
- private Invoice emptyInvoice() {
+ private Bill emptyBill() {
var start = clock.instant().atZone(ZoneOffset.UTC);
var end = clock.instant().atZone(ZoneOffset.UTC);
- return new Invoice(Invoice.Id.of("empty"), TenantName.defaultName(), Invoice.StatusHistory.open(clock), List.of(), start, end);
+ return new Bill(Bill.Id.of("empty"), TenantName.defaultName(), Bill.StatusHistory.open(clock), List.of(), start, end);
}
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java
new file mode 100644
index 00000000000..d64d6e3ea04
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java
@@ -0,0 +1,23 @@
+package com.yahoo.vespa.hosted.controller.api.integration.billing;
+
+import java.util.Optional;
+
+/**
+ * Registry of all current plans we have support for
+ *
+ * @author ogronnesby
+ */
+public interface PlanRegistry {
+
+ /** Get the default plan */
+ Plan defaultPlan();
+
+ /** Get a plan given a plan ID */
+ Optional<Plan> plan(PlanId planId);
+
+ /** Get a plan give a plan ID */
+ default Optional<Plan> plan(String planId) {
+ return plan(PlanId.from(planId));
+ }
+
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java
new file mode 100644
index 00000000000..60eddbd24ff
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java
@@ -0,0 +1,122 @@
+package com.yahoo.vespa.hosted.controller.api.integration.billing;
+
+import com.yahoo.config.provision.NodeResources;
+import com.yahoo.vespa.hosted.controller.api.integration.resource.CostInfo;
+import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceUsage;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public class PlanRegistryMock implements PlanRegistry {
+
+ public static final Plan freeTrial = new MockPlan("trial", false, 0, 0, 0, 200, "Free Trial - for testing purposes");
+ public static final Plan paidPlan = new MockPlan("paid", true, "0.09", "0.009", "0.0003", 500, "Paid Plan - for testing purposes");
+ public static final Plan nonePlan = new MockPlan("none", false, 0, 0, 0, 0, "None Plan - for testing purposes");
+
+ @Override
+ public Plan defaultPlan() {
+ return freeTrial;
+ }
+
+ @Override
+ public Optional<Plan> plan(PlanId planId) {
+ return Stream.of(freeTrial, paidPlan, nonePlan)
+ .filter(p -> p.id().equals(planId))
+ .findAny();
+ }
+
+ private static class MockPlan implements Plan {
+ private final PlanId planId;
+ private final String description;
+ private final CostCalculator costCalculator;
+ private final QuotaCalculator quotaCalculator;
+ private final boolean billed;
+
+ public MockPlan(String planId, boolean billed, double cpuPrice, double memPrice, double dgbPrice, int quota, String description) {
+ this(PlanId.from(planId), billed, new MockCostCalculator(cpuPrice, memPrice, dgbPrice), () -> Quota.unlimited().withBudget(quota), description);
+ }
+
+ public MockPlan(String planId, boolean billed, String cpuPrice, String memPrice, String dgbPrice, int quota, String description) {
+ this(PlanId.from(planId), billed, new MockCostCalculator(cpuPrice, memPrice, dgbPrice), () -> Quota.unlimited().withBudget(quota), description);
+ }
+
+ public MockPlan(PlanId planId, boolean billed, MockCostCalculator calculator, QuotaCalculator quota, String description) {
+ this.planId = planId;
+ this.billed = billed;
+ this.costCalculator = calculator;
+ this.quotaCalculator = quota;
+ this.description = description;
+ }
+
+ @Override
+ public PlanId id() {
+ return planId;
+ }
+
+ @Override
+ public String displayName() {
+ return description;
+ }
+
+ @Override
+ public CostCalculator calculator() {
+ return costCalculator;
+ }
+
+ @Override
+ public QuotaCalculator quota() {
+ return quotaCalculator;
+ }
+
+ @Override
+ public boolean isBilled() {
+ return billed;
+ }
+ }
+
+ private static class MockCostCalculator implements CostCalculator {
+ private static final BigDecimal millisPerHour = BigDecimal.valueOf(60 * 60 * 1000);
+ private final BigDecimal cpuHourCost;
+ private final BigDecimal memHourCost;
+ private final BigDecimal dgbHourCost;
+
+ public MockCostCalculator(String cpuPrice, String memPrice, String dgbPrice) {
+ this(new BigDecimal(cpuPrice), new BigDecimal(memPrice), new BigDecimal(dgbPrice));
+ }
+
+ public MockCostCalculator(double cpuPrice, double memPrice, double dgbPrice) {
+ this(BigDecimal.valueOf(cpuPrice), BigDecimal.valueOf(memPrice), BigDecimal.valueOf(dgbPrice));
+ }
+
+ public MockCostCalculator(BigDecimal cpuPrice, BigDecimal memPrice, BigDecimal dgbPrice) {
+ this.cpuHourCost = cpuPrice;
+ this.memHourCost = memPrice;
+ this.dgbHourCost = dgbPrice;
+ }
+
+ @Override
+ public CostInfo calculate(ResourceUsage usage) {
+ var cpuCost = usage.getCpuMillis().multiply(cpuHourCost).divide(millisPerHour, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP);
+ var memCost = usage.getMemoryMillis().multiply(memHourCost).divide(millisPerHour, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP);
+ var dgbCost = usage.getDiskMillis().multiply(dgbHourCost).divide(millisPerHour, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP);
+
+ return new CostInfo(
+ usage.getApplicationId(),
+ usage.getZoneId(),
+ usage.getCpuMillis().divide(millisPerHour, RoundingMode.HALF_UP),
+ usage.getMemoryMillis().divide(millisPerHour, RoundingMode.HALF_UP),
+ usage.getDiskMillis().divide(millisPerHour, RoundingMode.HALF_UP),
+ cpuCost,
+ memCost,
+ dgbCost
+ );
+ }
+
+ @Override
+ public double calculate(NodeResources resources) {
+ return resources.cost();
+ }
+ }
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java
new file mode 100644
index 00000000000..2f277193231
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java
@@ -0,0 +1,51 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.api.integration.resource;
+
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.TenantName;
+
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.temporal.ChronoUnit;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author olaa
+ */
+public interface ResourceDatabaseClient {
+
+ void writeResourceSnapshots(Collection<ResourceSnapshot> snapshots);
+
+ List<ResourceSnapshot> getResourceSnapshotsForMonth(TenantName tenantName, ApplicationName applicationName, YearMonth month);
+
+ List<ResourceUsage> getResourceSnapshotsForPeriod(TenantName tenantName, long startTimestamp, long endTimestamp);
+
+ void refreshMaterializedView();
+
+ Set<YearMonth> getMonthsWithSnapshotsForTenant(TenantName tenantName);
+
+ List<ResourceSnapshot> getRawSnapshotHistoryForTenant(TenantName tenantName, YearMonth yearMonth);
+
+ Set<TenantName> getTenants();
+
+ default List<ResourceUsage> getResourceSnapshotsForMonth(TenantName tenantName, YearMonth month) {
+ return getResourceSnapshotsForPeriod(tenantName, getMonthStartTimeStamp(month), getMonthEndTimeStamp(month));
+ }
+
+ private long getMonthStartTimeStamp(YearMonth month) {
+ LocalDate startOfMonth = LocalDate.of(month.getYear(), month.getMonth(), 1);
+ return startOfMonth.atStartOfDay(java.time.ZoneId.of("UTC"))
+ .toInstant()
+ .toEpochMilli();
+ }
+ private long getMonthEndTimeStamp(YearMonth month) {
+ LocalDate startOfMonth = LocalDate.of(month.getYear(), month.getMonth(), 1);
+ return startOfMonth.plus(1, ChronoUnit.MONTHS)
+ .atStartOfDay(java.time.ZoneId.of("UTC"))
+ .toInstant()
+ .toEpochMilli();
+ }
+
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java
new file mode 100644
index 00000000000..5a4d250ea9d
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java
@@ -0,0 +1,146 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.api.integration.resource;
+
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry;
+
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * @author olaa
+ */
+public class ResourceDatabaseClientMock implements ResourceDatabaseClient {
+
+ PlanRegistry planRegistry;
+ Map<TenantName, Plan> planMap = new HashMap<>();
+ List<ResourceSnapshot> resourceSnapshots = new ArrayList<>();
+ private boolean hasRefreshedMaterializedView = false;
+
+ public ResourceDatabaseClientMock(PlanRegistry planRegistry) {
+ this.planRegistry = planRegistry;
+ }
+
+ @Override
+ public void writeResourceSnapshots(Collection<ResourceSnapshot> items) {
+ this.resourceSnapshots.addAll(items);
+ }
+
+ @Override
+ public List<ResourceSnapshot> getResourceSnapshotsForMonth(TenantName tenantName, ApplicationName applicationName, YearMonth month) {
+ return resourceSnapshots.stream()
+ .filter(resourceSnapshot -> {
+ LocalDate snapshotDate = LocalDate.ofInstant(resourceSnapshot.getTimestamp(), ZoneId.of("UTC"));
+ return YearMonth.from(snapshotDate).equals(month) &&
+ snapshotDate.getYear() == month.getYear() &&
+ resourceSnapshot.getApplicationId().tenant().equals(tenantName) &&
+ resourceSnapshot.getApplicationId().application().equals(applicationName);
+ })
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public Set<YearMonth> getMonthsWithSnapshotsForTenant(TenantName tenantName) {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public List<ResourceSnapshot> getRawSnapshotHistoryForTenant(TenantName tenantName, YearMonth yearMonth) {
+ return resourceSnapshots;
+ }
+
+ @Override
+ public Set<TenantName> getTenants() {
+ return resourceSnapshots.stream()
+ .map(snapshot -> snapshot.getApplicationId().tenant())
+ .collect(Collectors.toSet());
+ }
+
+ private List<ResourceUsage> resourceUsageFromSnapshots(Plan plan, List<ResourceSnapshot> snapshots) {
+ snapshots.sort(Comparator.comparing(ResourceSnapshot::getTimestamp));
+
+ return IntStream.range(0, snapshots.size())
+ .mapToObj(idx -> {
+ var a = snapshots.get(idx);
+ var b = (idx + 1) < snapshots.size() ? snapshots.get(idx + 1) : null;
+ var start = a.getTimestamp();
+ var end = Optional.ofNullable(b).map(ResourceSnapshot::getTimestamp).orElse(start.plusSeconds(120));
+ var d = BigDecimal.valueOf(Duration.between(start, end).toMillis());
+ return new ResourceUsage(
+ a.getApplicationId(),
+ a.getZoneId(),
+ plan,
+ BigDecimal.valueOf(a.getCpuCores()).multiply(d),
+ BigDecimal.valueOf(a.getMemoryGb()).multiply(d),
+ BigDecimal.valueOf(a.getDiskGb()).multiply(d)
+ );
+ })
+ .collect(Collectors.toList());
+ }
+
+ private ResourceUsage resourceUsageAdd(ResourceUsage a, ResourceUsage b) {
+ assert a.getApplicationId().equals(b.getApplicationId());
+ assert a.getZoneId().equals(b.getZoneId());
+ assert a.getPlan().equals(b.getPlan());
+ return new ResourceUsage(
+ a.getApplicationId(),
+ a.getZoneId(),
+ a.getPlan(),
+ a.getCpuMillis().add(b.getCpuMillis()),
+ a.getMemoryMillis().add(b.getMemoryMillis()),
+ a.getDiskMillis().add(b.getDiskMillis())
+ );
+ }
+
+ @Override
+ public List<ResourceUsage> getResourceSnapshotsForPeriod(TenantName tenantName, long start, long end) {
+ var tenantPlan = planMap.getOrDefault(tenantName, planRegistry.defaultPlan());
+
+ var snapshotsPerDeployment = resourceSnapshots.stream()
+ .filter(snapshot -> snapshot.getTimestamp().isAfter(Instant.ofEpochMilli(start)))
+ .filter(snapshot -> snapshot.getTimestamp().isBefore(Instant.ofEpochMilli(end)))
+ .filter(snapshot -> snapshot.getApplicationId().tenant().equals(tenantName))
+ .collect(Collectors.groupingBy(
+ usage -> Objects.hash(usage.getApplicationId(), usage.getZoneId(), tenantPlan.id().value())
+ ))
+ .values().stream()
+ .map(snapshots -> resourceUsageFromSnapshots(tenantPlan, snapshots))
+ .map(usages -> usages.stream().reduce(this::resourceUsageAdd))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toList());
+
+ return snapshotsPerDeployment;
+ }
+
+ @Override
+ public void refreshMaterializedView() {
+ hasRefreshedMaterializedView = true;
+ }
+
+ public void setPlan(TenantName tenant, Plan plan) {
+ planMap.put(tenant, plan);
+ }
+
+ public boolean hasRefreshedMaterializedView() {
+ return hasRefreshedMaterializedView;
+ }
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java
index a8d9c4b1f8a..319b9239ae4 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java
@@ -31,6 +31,10 @@ public class ResourceSnapshot {
this.zoneId = zoneId;
}
+ public static ResourceSnapshot from(ApplicationId applicationId, int nodes, double cpuCores, double memoryGb, double diskGb, Instant timestamp, ZoneId zoneId) {
+ return new ResourceSnapshot(applicationId, cpuCores * nodes, memoryGb * nodes, diskGb * nodes, timestamp, zoneId);
+ }
+
public static ResourceSnapshot from(List<Node> nodes, Instant timestamp, ZoneId zoneId) {
Set<ApplicationId> applicationIds = nodes.stream()
.filter(node -> node.owner().isPresent())
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
index b595d3fbe17..b8c8ce4e63a 100644
--- 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
@@ -18,9 +18,9 @@ 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.CollectionMethod;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PaymentInstrument;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Invoice;
import com.yahoo.vespa.hosted.controller.api.integration.billing.InstrumentOwner;
import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
@@ -99,23 +99,23 @@ public class BillingApiHandler extends LoggingRequestHandler {
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 getAllInvoices();
+ if (path.matches("/billing/v1/invoice/export")) return getAllBills();
if (path.matches("/billing/v1/invoice/tenant/{tenant}/line-item")) return getLineItems(path.get("tenant"));
return ErrorResponse.notFoundError("Nothing at " + path);
}
- private HttpResponse getAllInvoices() {
- var invoices = billingController.getInvoices();
+ private HttpResponse getAllBills() {
+ var bills = billingController.getBills();
var headers = new String[]{ "ID", "Tenant", "From", "To", "CpuHours", "MemoryHours", "DiskHours", "Cpu", "Memory", "Disk", "Additional" };
- var rows = invoices.stream()
- .map(invoice -> {
+ var rows = bills.stream()
+ .map(bill -> {
return new Object[] {
- invoice.id().value(), invoice.tenant().value(),
- invoice.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE),
- invoice.getEndTime().format(DateTimeFormatter.ISO_LOCAL_DATE),
- invoice.sumCpuHours(), invoice.sumMemoryHours(), invoice.sumDiskHours(),
- invoice.sumCpuCost(), invoice.sumMemoryCost(), invoice.sumDiskCost(),
- invoice.sumAdditionalCost()
+ bill.id().value(), bill.tenant().value(),
+ bill.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE),
+ bill.getEndTime().format(DateTimeFormatter.ISO_LOCAL_DATE),
+ bill.sumCpuHours(), bill.sumMemoryHours(), bill.sumDiskHours(),
+ bill.sumCpuCost(), bill.sumMemoryCost(), bill.sumDiskCost(),
+ bill.sumAdditionalCost()
};
})
.collect(Collectors.toList());
@@ -138,8 +138,8 @@ public class BillingApiHandler extends LoggingRequestHandler {
}
private HttpResponse handlePOST(Path path, HttpRequest request, String userId) {
- if (path.matches("/billing/v1/invoice")) return createInvoice(request, userId);
- if (path.matches("/billing/v1/invoice/{invoice-id}/status")) return setInvoiceStatus(request, path.get("invoice-id"));
+ 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"));
if (path.matches("/billing/v1/invoice/tenant/{tenant}/line-item")) return addLineItem(request, path.get("tenant"));
return ErrorResponse.notFoundError("Nothing at " + path);
@@ -190,7 +190,7 @@ public class BillingApiHandler extends LoggingRequestHandler {
private HttpResponse getBillingAllTenants(String until) {
try {
var untilDate = untilParameter(until);
- var uncommittedInvoices = billingController.createUncommittedInvoices(untilDate);
+ var uncommittedBills = billingController.createUncommittedBills(untilDate);
var slime = new Slime();
var root = slime.setObject();
@@ -198,12 +198,12 @@ public class BillingApiHandler extends LoggingRequestHandler {
var tenants = root.setArray("tenants");
tenantController.asList().stream().sorted(Comparator.comparing(Tenant::name)).forEach(tenant -> {
- var invoice = uncommittedInvoices.get(tenant.name());
+ 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"), invoice);
+ renderCurrentUsage(tc.setObject("current"), bill);
renderAdditionalItems(tc.setObject("additional").setArray("items"), billingController.getUnusedLineItems(tenant.name()));
billingController.getDefaultInstrument(tenant.name()).ifPresent(card ->
@@ -232,14 +232,14 @@ public class BillingApiHandler extends LoggingRequestHandler {
return new MessageResponse("Added line item for tenant " + tenant);
}
- private HttpResponse setInvoiceStatus(HttpRequest request, String invoiceId) {
+ private HttpResponse setBillStatus(HttpRequest request, String billId) {
Inspector inspector = inspectorOrThrow(request);
String status = getInspectorFieldOrThrow(inspector, "status");
- billingController.updateInvoiceStatus(Invoice.Id.of(invoiceId), userIdOrThrow(request), status);
- return new MessageResponse("Updated status of invoice " + invoiceId);
+ billingController.updateBillStatus(Bill.Id.of(billId), userIdOrThrow(request), status);
+ return new MessageResponse("Updated status of invoice " + billId);
}
- private HttpResponse createInvoice(HttpRequest request, String userId) {
+ private HttpResponse createBill(HttpRequest request, String userId) {
Inspector inspector = inspectorOrThrow(request);
TenantName tenantName = TenantName.from(getInspectorFieldOrThrow(inspector, "tenant"));
@@ -248,12 +248,12 @@ public class BillingApiHandler extends LoggingRequestHandler {
ZonedDateTime startTime = startDate.atStartOfDay(ZoneId.of("UTC"));
ZonedDateTime endTime = endDate.atStartOfDay(ZoneId.of("UTC"));
- var invoiceId = billingController.createInvoiceForPeriod(tenantName, startTime, endTime, userId);
+ var billId = billingController.createBillForPeriod(tenantName, startTime, endTime, userId);
Slime slime = new Slime();
Cursor root = slime.setObject();
- root.setString("message", "Created invoice with ID " + invoiceId.value());
- root.setString("id", invoiceId.value());
+ root.setString("message", "Created invoice with ID " + billId.value());
+ root.setString("id", billId.value());
return new SlimeJsonResponse(slime);
}
@@ -278,7 +278,7 @@ public class BillingApiHandler extends LoggingRequestHandler {
getPlanForTenant(root, tenantId);
renderCurrentUsage(root.setObject("current"), getCurrentUsageForTenant(tenantId, untilDate));
renderAdditionalItems(root.setObject("additional").setArray("items"), billingController.getUnusedLineItems(tenantId));
- renderInvoices(root.setArray("bills"), getInvoicesForTenant(tenantId));
+ renderBills(root.setArray("bills"), getBillsForTenant(tenantId));
billingController.getDefaultInstrument(tenantId).ifPresent( card ->
renderInstrument(root.setObject("payment"), card)
@@ -328,7 +328,7 @@ public class BillingApiHandler extends LoggingRequestHandler {
}
- private void renderCurrentUsage(Cursor cursor, Invoice currentUsage) {
+ private void renderCurrentUsage(Cursor cursor, Bill currentUsage) {
if (currentUsage == null) return;
cursor.setString("amount", currentUsage.sum().toPlainString());
cursor.setString("status", "accrued");
@@ -340,46 +340,46 @@ public class BillingApiHandler extends LoggingRequestHandler {
});
}
- private void renderAdditionalItems(Cursor cursor, List<Invoice.LineItem> items) {
+ private void renderAdditionalItems(Cursor cursor, List<Bill.LineItem> items) {
items.forEach(item -> {
renderLineItemToCursor(cursor.addObject(), item);
});
}
- private Invoice getCurrentUsageForTenant(TenantName tenant, LocalDate until) {
- return billingController.createUncommittedInvoice(tenant, until);
+ private Bill getCurrentUsageForTenant(TenantName tenant, LocalDate until) {
+ return billingController.createUncommittedBill(tenant, until);
}
- private List<Invoice> getInvoicesForTenant(TenantName tenant) {
- return billingController.getInvoicesForTenant(tenant);
+ private List<Bill> getBillsForTenant(TenantName tenant) {
+ return billingController.getBillsForTenant(tenant);
}
- private void renderInvoices(Cursor cursor, List<Invoice> invoices) {
- invoices.forEach(invoice -> {
- var invoiceCursor = cursor.addObject();
- renderInvoiceToCursor(invoiceCursor, invoice);
+ private void renderBills(Cursor cursor, List<Bill> bills) {
+ bills.forEach(bill -> {
+ var billCursor = cursor.addObject();
+ renderBillToCursor(billCursor, bill);
});
}
- private void renderInvoiceToCursor(Cursor invoiceCursor, Invoice invoice) {
- invoiceCursor.setString("id", invoice.id().value());
- invoiceCursor.setString("from", invoice.getStartTime().format(DATE_TIME_FORMATTER));
- invoiceCursor.setString("to", invoice.getEndTime().format(DATE_TIME_FORMATTER));
+ private void renderBillToCursor(Cursor billCursor, Bill bill) {
+ billCursor.setString("id", bill.id().value());
+ billCursor.setString("from", bill.getStartTime().format(DATE_TIME_FORMATTER));
+ billCursor.setString("to", bill.getEndTime().format(DATE_TIME_FORMATTER));
- invoiceCursor.setString("amount", invoice.sum().toString());
- invoiceCursor.setString("status", invoice.status());
- var statusCursor = invoiceCursor.setArray("statusHistory");
- renderStatusHistory(statusCursor, invoice.statusHistory());
+ billCursor.setString("amount", bill.sum().toString());
+ billCursor.setString("status", bill.status());
+ var statusCursor = billCursor.setArray("statusHistory");
+ renderStatusHistory(statusCursor, bill.statusHistory());
- var lineItemsCursor = invoiceCursor.setArray("items");
- invoice.lineItems().forEach(lineItem -> {
+ var lineItemsCursor = billCursor.setArray("items");
+ bill.lineItems().forEach(lineItem -> {
var itemCursor = lineItemsCursor.addObject();
renderLineItemToCursor(itemCursor, lineItem);
});
}
- private void renderStatusHistory(Cursor cursor, Invoice.StatusHistory statusHistory) {
+ private void renderStatusHistory(Cursor cursor, Bill.StatusHistory statusHistory) {
statusHistory.getHistory()
.entrySet()
.stream()
@@ -390,7 +390,7 @@ public class BillingApiHandler extends LoggingRequestHandler {
});
}
- private void renderLineItemToCursor(Cursor cursor, Invoice.LineItem lineItem) {
+ private void renderLineItemToCursor(Cursor cursor, Bill.LineItem lineItem) {
cursor.setString("id", lineItem.id());
cursor.setString("description", lineItem.description());
cursor.setString("amount", lineItem.amount().toString());
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 58afed4143f..630305b0ab4 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
@@ -15,9 +15,9 @@ import com.yahoo.slime.Type;
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.BillingController;
import com.yahoo.vespa.hosted.controller.api.integration.billing.CollectionMethod;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Invoice;
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;
@@ -138,7 +138,7 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
var tenant = tenants.require(tenantName, CloudTenant.class);
var slime = new Slime();
- invoicesSummaryToSlime(slime.setObject().setArray("invoices"), billing.getInvoicesForTenant(tenant.name()));
+ invoicesSummaryToSlime(slime.setObject().setArray("invoices"), billing.getBillsForTenant(tenant.name()));
return slime;
}
@@ -148,7 +148,7 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
var invoiceId = requestContext.pathParameters().getStringOrThrow("invoice");
var format = requestContext.queryParameters().getString("format").orElse("json");
- var invoice = billing.getInvoicesForTenant(tenant.name()).stream()
+ var invoice = billing.getBillsForTenant(tenant.name()).stream()
.filter(inv -> inv.id().value().equals(invoiceId))
.findAny()
.orElseThrow(RestApiException.NotFound::new);
@@ -178,7 +178,7 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
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 usage = billing.createUncommittedBill(tenant.name(), untilAt.atZone(ZoneOffset.UTC).toLocalDate());
var slime = new Slime();
usageToSlime(slime.setObject(), usage);
@@ -189,7 +189,7 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
private Slime accountant(RestApi.RequestContext requestContext) {
var untilAt = untilParameter(requestContext);
- var usagePerTenant = billing.createUncommittedInvoices(untilAt.atZone(ZoneOffset.UTC).toLocalDate());
+ var usagePerTenant = billing.createUncommittedBills(untilAt.atZone(ZoneOffset.UTC).toLocalDate());
var response = new Slime();
var tenantsResponse = response.setObject().setArray("tenants");
@@ -199,8 +199,8 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
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"));
+ tenantResponse.setString("lastBill", usage.map(Bill::getStartTime).map(DateTimeFormatter.ISO_DATE::format).orElse(null));
+ tenantResponse.setString("unbilled", usage.map(Bill::sum).map(BigDecimal::toPlainString).orElse("0.00"));
});
return response;
@@ -211,7 +211,7 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
var tenant = tenants.require(tenantName, CloudTenant.class);
var untilAt = untilParameter(requestContext);
- var usage = billing.createUncommittedInvoice(tenant.name(), untilAt.atZone(ZoneOffset.UTC).toLocalDate());
+ var usage = billing.createUncommittedBill(tenant.name(), untilAt.atZone(ZoneOffset.UTC).toLocalDate());
var slime = new Slime();
toSlime(slime.setObject(), usage);
@@ -230,7 +230,7 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
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());
+ var invoiceId = billing.createBillForPeriod(tenant.name(), startAt, endAt, security.principal().getName());
// TODO: Make a redirect to the bill itself
return new MessageResponse("Created bill " + invoiceId.value());
@@ -239,36 +239,36 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
// --------- INVOICE RENDERING ----------
- private void invoicesSummaryToSlime(Cursor slime, List<Invoice> invoices) {
- invoices.forEach(invoice -> invoiceSummaryToSlime(slime.addObject(), invoice));
+ private void invoicesSummaryToSlime(Cursor slime, List<Bill> bills) {
+ bills.forEach(invoice -> invoiceSummaryToSlime(slime.addObject(), invoice));
}
- private void invoiceSummaryToSlime(Cursor slime, Invoice invoice) {
- slime.setString("id", invoice.id().value());
- slime.setString("from", invoice.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
- slime.setString("to", invoice.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
- slime.setString("total", invoice.sum().toString());
- slime.setString("status", invoice.status());
+ private void invoiceSummaryToSlime(Cursor slime, Bill bill) {
+ slime.setString("id", bill.id().value());
+ slime.setString("from", bill.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
+ slime.setString("to", bill.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
+ slime.setString("total", bill.sum().toString());
+ slime.setString("status", bill.status());
}
- private void usageToSlime(Cursor slime, Invoice invoice) {
- slime.setString("from", invoice.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
- slime.setString("to", invoice.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
- slime.setString("total", invoice.sum().toString());
- toSlime(slime.setArray("items"), invoice.lineItems());
+ private void usageToSlime(Cursor slime, Bill bill) {
+ slime.setString("from", bill.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
+ slime.setString("to", bill.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
+ slime.setString("total", bill.sum().toString());
+ toSlime(slime.setArray("items"), bill.lineItems());
}
- private void toSlime(Cursor slime, Invoice invoice) {
- slime.setString("id", invoice.id().value());
- slime.setString("from", invoice.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
- slime.setString("to", invoice.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
- slime.setString("total", invoice.sum().toString());
- slime.setString("status", invoice.status());
- toSlime(slime.setArray("statusHistory"), invoice.statusHistory());
- toSlime(slime.setArray("items"), invoice.lineItems());
+ private void toSlime(Cursor slime, Bill bill) {
+ slime.setString("id", bill.id().value());
+ slime.setString("from", bill.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
+ slime.setString("to", bill.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
+ slime.setString("total", bill.sum().toString());
+ slime.setString("status", bill.status());
+ toSlime(slime.setArray("statusHistory"), bill.statusHistory());
+ toSlime(slime.setArray("items"), bill.lineItems());
}
- private void toSlime(Cursor slime, Invoice.StatusHistory history) {
+ private void toSlime(Cursor slime, Bill.StatusHistory history) {
history.getHistory().forEach((key, value) -> {
var c = slime.addObject();
c.setString("at", key.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
@@ -276,11 +276,11 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
});
}
- private void toSlime(Cursor slime, List<Invoice.LineItem> items) {
+ private void toSlime(Cursor slime, List<Bill.LineItem> items) {
items.forEach(item -> toSlime(slime.addObject(), item));
}
- private void toSlime(Cursor slime, Invoice.LineItem item) {
+ private void toSlime(Cursor slime, Bill.LineItem item) {
slime.setString("id", item.id());
slime.setString("description", item.description());
slime.setString("amount",item.amount().toString());
@@ -304,14 +304,14 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
cost.ifPresent(c -> slime.setString("cost", c.toString()));
}
- private List<Object[]> toCsv(Invoice invoice) {
+ private List<Object[]> toCsv(Bill bill) {
return List.<Object[]>of(new Object[]{
- invoice.id().value(), invoice.tenant().value(),
- invoice.getStartTime().format(DateTimeFormatter.ISO_DATE),
- invoice.getEndTime().format(DateTimeFormatter.ISO_DATE),
- invoice.sumCpuHours(), invoice.sumMemoryHours(), invoice.sumDiskHours(),
- invoice.sumCpuCost(), invoice.sumMemoryCost(), invoice.sumDiskCost(),
- invoice.sumAdditionalCost()
+ bill.id().value(), bill.tenant().value(),
+ bill.getStartTime().format(DateTimeFormatter.ISO_DATE),
+ bill.getEndTime().format(DateTimeFormatter.ISO_DATE),
+ bill.sumCpuHours(), bill.sumMemoryHours(), bill.sumDiskHours(),
+ bill.sumCpuCost(), bill.sumMemoryCost(), bill.sumDiskCost(),
+ bill.sumAdditionalCost()
});
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java
index 6ddf0ec76ed..f64c54ff24d 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java
@@ -17,7 +17,11 @@ import com.yahoo.vespa.hosted.controller.api.integration.aws.MockCloudEventFetch
import com.yahoo.vespa.hosted.controller.api.integration.aws.MockResourceTagger;
import com.yahoo.vespa.hosted.controller.api.integration.aws.ResourceTagger;
import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingDatabaseClient;
import com.yahoo.vespa.hosted.controller.api.integration.billing.MockBillingController;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingDatabaseClientMock;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistryMock;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMock;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidator;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidatorMock;
@@ -28,6 +32,8 @@ import com.yahoo.vespa.hosted.controller.api.integration.horizon.MockHorizonClie
import com.yahoo.vespa.hosted.controller.api.integration.organization.MockContactRetriever;
import com.yahoo.vespa.hosted.controller.api.integration.organization.MockIssueHandler;
import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumerMock;
+import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClientMock;
+import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClient;
import com.yahoo.vespa.hosted.controller.api.integration.routing.GlobalRoutingService;
import com.yahoo.vespa.hosted.controller.api.integration.routing.MemoryGlobalRoutingService;
import com.yahoo.vespa.hosted.controller.api.integration.secrets.NoopTenantSecretService;
@@ -77,6 +83,9 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg
private final MockChangeRequestClient changeRequestClient = new MockChangeRequestClient();
private final AccessControlService accessControlService = new MockAccessControlService();
private final HorizonClient horizonClient = new MockHorizonClient();
+ private final PlanRegistry planRegistry = new PlanRegistryMock();
+ private final ResourceDatabaseClient resourceDb = new ResourceDatabaseClientMock(planRegistry);
+ private final BillingDatabaseClient billingDb = new BillingDatabaseClientMock(clock, planRegistry);
public ServiceRegistryMock(SystemName system) {
this.zoneRegistryMock = new ZoneRegistryMock(system);
@@ -243,6 +252,21 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg
return horizonClient;
}
+ @Override
+ public PlanRegistry planRegistry() {
+ return planRegistry;
+ }
+
+ @Override
+ public ResourceDatabaseClient resourceDatabase() {
+ return resourceDb;
+ }
+
+ @Override
+ public BillingDatabaseClient billingDatabase() {
+ return billingDb;
+ }
+
public ConfigServerMock configServerMock() {
return configServerMock;
}
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
index 8bc32d262f1..3c4ed8bc8df 100644
--- 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
@@ -3,8 +3,8 @@ 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.CollectionMethod;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Invoice;
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.role.Role;
@@ -19,7 +19,6 @@ import org.junit.Test;
import java.io.File;
import java.math.BigDecimal;
import java.time.LocalDate;
-import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.List;
@@ -95,10 +94,10 @@ public class BillingApiHandlerTest extends ControllerContainerCloudTest {
@Test
public void response_list_bills() {
- var invoice = createInvoice();
+ var bill = createBill();
- billingController.addInvoice(tenant, invoice, true);
- billingController.addInvoice(tenant, invoice, false);
+ billingController.addBill(tenant, bill, true);
+ billingController.addBill(tenant, bill, false);
billingController.setPlan(tenant, PlanId.from("some-plan"), true);
var request = request("/billing/v1/tenant/tenant1/billing?until=2020-05-28").roles(tenantRole);
@@ -107,9 +106,9 @@ public class BillingApiHandlerTest extends ControllerContainerCloudTest {
}
@Test
- public void test_invoice_creation() {
- var invoices = billingController.getInvoicesForTenant(tenant);
- assertEquals(0, invoices.size());
+ public 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)
@@ -120,11 +119,11 @@ public class BillingApiHandlerTest extends ControllerContainerCloudTest {
request.roles(financeAdmin);
tester.assertResponse(request, new File("invoice-creation-response"));
- invoices = billingController.getInvoicesForTenant(tenant);
- assertEquals(1, invoices.size());
- Invoice invoice = invoices.get(0);
- assertEquals(invoice.getStartTime().toString(), "2020-04-20T00:00Z[UTC]");
- assertEquals(invoice.getEndTime().toString(), "2020-05-20T00:00Z[UTC]");
+ bills = billingController.getBillsForTenant(tenant);
+ assertEquals(1, bills.size());
+ Bill bill = bills.get(0);
+ assertEquals(bill.getStartTime().toString(), "2020-04-20T00:00Z[UTC]");
+ assertEquals(bill.getEndTime().toString(), "2020-05-20T00:00Z[UTC]");
}
@Test
@@ -143,7 +142,7 @@ public class BillingApiHandlerTest extends ControllerContainerCloudTest {
var lineItems = billingController.getUnusedLineItems(tenant);
Assert.assertEquals(1, lineItems.size());
- Invoice.LineItem lineItem = lineItems.get(0);
+ Bill.LineItem lineItem = lineItems.get(0);
assertEquals("some description", lineItem.description());
assertEquals(new BigDecimal("123.45"), lineItem.amount());
@@ -155,7 +154,7 @@ public class BillingApiHandlerTest extends ControllerContainerCloudTest {
@Test
public void adding_new_status() {
- billingController.addInvoice(tenant, createInvoice(), true);
+ billingController.addBill(tenant, createBill(), true);
var requestBody = "{\"status\":\"DONE\"}";
var request = request("/billing/v1/invoice/id-1/status", POST)
@@ -163,21 +162,21 @@ public class BillingApiHandlerTest extends ControllerContainerCloudTest {
.roles(financeAdmin);
tester.assertResponse(request, "{\"message\":\"Updated status of invoice id-1\"}");
- var invoice = billingController.getInvoicesForTenant(tenant).get(0);
- assertEquals("DONE", invoice.status());
+ var bill = billingController.getBillsForTenant(tenant).get(0);
+ assertEquals("DONE", bill.status());
}
@Test
- public void list_all_uninvoiced_items() {
+ public 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 invoice = createInvoice();
+ var bill = createBill();
billingController.setPlan(tenant, PlanId.from("some-plan"), true);
billingController.setPlan(tenant2, PlanId.from("some-plan"), true);
- billingController.addInvoice(tenant, invoice, false);
+ billingController.addBill(tenant, bill, false);
billingController.addLineItem(tenant, "support", new BigDecimal("42"), "Smith");
- billingController.addInvoice(tenant2, invoice, false);
+ billingController.addBill(tenant2, bill, false);
var request = request("/billing/v1/billing?until=2020-05-28").roles(financeAdmin);
@@ -195,8 +194,8 @@ public class BillingApiHandlerTest extends ControllerContainerCloudTest {
@Test
public void csv_export() {
- var invoice = createInvoice();
- billingController.addInvoice(tenant, invoice, true);
+ 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);
}
@@ -222,12 +221,12 @@ public class BillingApiHandlerTest extends ControllerContainerCloudTest {
assertEquals(CollectionMethod.INVOICE, billingController.getCollectionMethod(tenant));
}
- static Invoice createInvoice() {
+ static Bill createBill() {
var start = LocalDate.of(2020, 5, 23).atStartOfDay(ZoneOffset.UTC);
var end = start.plusDays(5);
- var statusHistory = new Invoice.StatusHistory(new TreeMap<>(Map.of(start, "OPEN")));
- return new Invoice(
- Invoice.Id.of("id-1"),
+ var statusHistory = new Bill.StatusHistory(new TreeMap<>(Map.of(start, "OPEN")));
+ return new Bill(
+ Bill.Id.of("id-1"),
TenantName.defaultName(),
statusHistory,
List.of(createLineItem(start)),
@@ -236,8 +235,8 @@ public class BillingApiHandlerTest extends ControllerContainerCloudTest {
);
}
- static Invoice.LineItem createLineItem(ZonedDateTime addedAt) {
- return new Invoice.LineItem(
+ static Bill.LineItem createLineItem(ZonedDateTime addedAt) {
+ return new Bill.LineItem(
"some-id",
"description",
new BigDecimal("123.00"),
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 e733f8e27d6..538e568804b 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
@@ -43,7 +43,7 @@ public class BillingApiHandlerV2Test extends ControllerContainerCloudTest {
var clock = (ManualClock) tester.controller().serviceRegistry().clock();
clock.setInstant(Instant.parse("2021-04-13T00:00:00Z"));
billingController = (MockBillingController) tester.serviceRegistry().billingController();
- billingController.addInvoice(tenant, BillingApiHandlerTest.createInvoice(), true);
+ billingController.addBill(tenant, BillingApiHandlerTest.createBill(), true);
}
@Override