diff options
Diffstat (limited to 'controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing')
24 files changed, 313 insertions, 75 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java index 664669d8e55..e7959d2057a 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java @@ -1,22 +1,18 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import java.math.BigDecimal; -import java.time.Clock; import java.time.LocalDate; -import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.SortedMap; -import java.util.TreeMap; import java.util.UUID; import java.util.function.Function; @@ -42,14 +38,22 @@ public class Bill { private final StatusHistory statusHistory; private final ZonedDateTime startTime; private final ZonedDateTime endTime; + private final String exportedId; - public Bill(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, tenant, statusHistory, lineItems, startTime, endTime, null); + } + + public Bill(Id id, TenantName tenant, StatusHistory statusHistory, List<LineItem> lineItems, + ZonedDateTime startTime, ZonedDateTime endTime, String exportedId) { this.id = id; this.tenant = tenant; this.lineItems = List.copyOf(lineItems); this.statusHistory = statusHistory; this.startTime = startTime; this.endTime = endTime; + this.exportedId = exportedId; } public Id id() { @@ -60,7 +64,7 @@ public class Bill { return tenant; } - public String status() { + public BillStatus status() { return statusHistory.current(); } @@ -80,6 +84,10 @@ public class Bill { return endTime; } + public Optional<String> getExportedId() { + return Optional.ofNullable(exportedId); + } + public LocalDate getStartDate() { return startTime.toLocalDate(); } @@ -196,6 +204,8 @@ public class Bill { private BigDecimal gpuCost; private NodeResources.Architecture architecture; private int majorVersion; + private CloudAccount cloudAccount; + private String exportedId; public LineItem(String id, String description, BigDecimal amount, String plan, String agent, ZonedDateTime addedAt) { this.id = id; @@ -204,10 +214,15 @@ public class Bill { this.plan = plan; this.agent = agent; this.addedAt = addedAt; + this.cloudAccount = CloudAccount.empty; } - public LineItem(String id, String description, BigDecimal amount, String plan, String agent, ZonedDateTime addedAt, ZonedDateTime startedAt, ZonedDateTime endedAt, ApplicationId applicationId, ZoneId zoneId, - BigDecimal cpuHours, BigDecimal memoryHours, BigDecimal diskHours, BigDecimal gpuHours, BigDecimal cpuCost, BigDecimal memoryCost, BigDecimal diskCost, BigDecimal gpuCost, NodeResources.Architecture architecture, int majorVersion) { + public LineItem(String id, String description, BigDecimal amount, String plan, String agent, ZonedDateTime addedAt, + ZonedDateTime startedAt, ZonedDateTime endedAt, ApplicationId applicationId, ZoneId zoneId, + BigDecimal cpuHours, BigDecimal memoryHours, BigDecimal diskHours, BigDecimal gpuHours, BigDecimal cpuCost, + BigDecimal memoryCost, BigDecimal diskCost, BigDecimal gpuCost, NodeResources.Architecture architecture, + int majorVersion, CloudAccount cloudAccount, String exportedId) + { this(id, description, amount, plan, agent, addedAt); this.startedAt = startedAt; this.endedAt = endedAt; @@ -227,6 +242,8 @@ public class Bill { this.architecture = architecture; this.majorVersion = majorVersion; this.gpuCost = gpuCost; + this.cloudAccount = cloudAccount; + this.exportedId = exportedId; } /** The opaque ID of this */ @@ -319,6 +336,14 @@ public class Bill { return majorVersion; } + public CloudAccount getCloudAccount() { + return cloudAccount; + } + + public Optional<String> getExportedId() { + return Optional.ofNullable(exportedId); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -359,28 +384,4 @@ public class Bill { } } - public static class StatusHistory { - SortedMap<ZonedDateTime, String> history; - - public StatusHistory(SortedMap<ZonedDateTime, String> history) { - this.history = history; - } - - public static StatusHistory open(Clock clock) { - var now = clock.instant().atZone(ZoneOffset.UTC); - return new StatusHistory( - new TreeMap<>(Map.of(now, "OPEN")) - ); - } - - public String current() { - return history.get(history.lastKey()); - } - - public SortedMap<ZonedDateTime, String> getHistory() { - return history; - } - - } - } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatus.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatus.java new file mode 100644 index 00000000000..4f35b47219a --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatus.java @@ -0,0 +1,33 @@ +package com.yahoo.vespa.hosted.controller.api.integration.billing; + +/** + * @author gjoranv + */ +public enum BillStatus { + OPEN, // All bills start in this state. The bill can be modified and exported/synced to external systems. + FROZEN, // Syncing to external systems is switched off. No changes can be made. + CLOSED, // End state for a valid bill. + VOID; // End state, indicating that the bill is not valid. + + // Legacy states, used by historical bills + private static final String LEGACY_ISSUED = "ISSUED"; + private static final String LEGACY_EXPORTED = "EXPORTED"; + private static final String LEGACY_CANCELED = "CANCELED"; + + private final String value; + + BillStatus() { + this.value = name(); + } + + public String value() { + return value; + } + + public static BillStatus from(String status) { + if (LEGACY_ISSUED.equals(status) || LEGACY_EXPORTED.equals(status)) return OPEN; + if (LEGACY_CANCELED.equals(status)) return VOID; + return Enum.valueOf(BillStatus.class, status.toUpperCase()); + } + +} 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 dfa0d4dccf2..0aae466feb2 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 @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import com.yahoo.config.provision.TenantName; @@ -90,7 +90,7 @@ public interface BillingController { boolean deleteInstrument(TenantName tenant, String userId, String instrumentId); /** Change the status of the given bill */ - void updateBillStatus(Bill.Id billId, String agent, String status); + void updateBillStatus(Bill.Id billId, String agent, BillStatus status); /** Add a line item to the given bill */ void addLineItem(TenantName tenant, String description, BigDecimal amount, Optional<Bill.Id> billId, String agent); @@ -130,7 +130,4 @@ public interface BillingController { default void updateCache(List<TenantName> tenants) {} - default String exportBill(Bill bill, String exportMethod, CloudTenant tenant) { - return "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 index 13229b650da..c5859cd7d2f 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import com.yahoo.config.provision.TenantName; @@ -69,7 +69,7 @@ public interface BillingDatabaseClient { * @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); + void setStatus(Bill.Id billId, String agent, BillStatus status); List<Bill.LineItem> getUnusedLineItems(TenantName tenantName); @@ -144,4 +144,15 @@ public interface BillingDatabaseClient { * Performs necessary maintenance operations */ void maintain(); + + /** + * Set the invoice id from an external system for the given bill + */ + void setExportedInvoiceId(Bill.Id billId, String invoiceId); + + /** + * Set the invoice item id from an external system for the given line item + */ + void setExportedInvoiceItemId(String lineItemId, String invoiceItemId); + } 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 index 3cee9e1ca2c..a6bcc9bf0ed 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import com.yahoo.config.provision.TenantName; @@ -26,9 +26,10 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { 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, StatusHistory> statuses = new HashMap<>(); private final Map<Bill.Id, ZonedDateTime> startTimes = new HashMap<>(); private final Map<Bill.Id, ZonedDateTime> endTimes = new HashMap<>(); + private final Map<Bill.Id, String> exportedInvoiceIds = 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")); @@ -53,7 +54,7 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { .findFirst(); } - public String getStatus(Bill.Id invoiceId) { + public BillStatus getStatus(Bill.Id invoiceId) { return statuses.get(invoiceId).current(); } @@ -61,7 +62,7 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { 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)); + statuses.computeIfAbsent(invoiceId, l -> StatusHistory.open(clock)); startTimes.put(invoiceId, startTime); endTimes.put(invoiceId, endTime); return invoiceId; @@ -71,10 +72,11 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { 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 status = statuses.getOrDefault(billId, 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)); + var exportedId = exportedInvoiceId(billId); + return invoice.map(tenant -> new Bill(billId, tenant, status, lines, start, end, exportedId)); } @Override @@ -88,8 +90,8 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { } @Override - public void setStatus(Bill.Id invoiceId, String agent, String status) { - statuses.computeIfAbsent(invoiceId, k -> Bill.StatusHistory.open(clock)) + public void setStatus(Bill.Id invoiceId, String agent, BillStatus status) { + statuses.computeIfAbsent(invoiceId, k -> StatusHistory.open(clock)) .getHistory() .put(ZonedDateTime.now(), status); } @@ -157,7 +159,7 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { var status = statuses.get(invoiceId); var start = startTimes.get(invoiceId); var end = endTimes.get(invoiceId); - return new Bill(invoiceId, tenant, status, items, start, end); + return new Bill(invoiceId, tenant, status, items, start, end, exportedInvoiceId(invoiceId)); }) .toList(); } @@ -171,11 +173,23 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { var status = statuses.get(invoiceId); var start = startTimes.get(invoiceId); var end = endTimes.get(invoiceId); - return new Bill(invoiceId, tenant, status, items, start, end); + return new Bill(invoiceId, tenant, status, items, start, end, exportedInvoiceId(invoiceId)); }) .toList(); } @Override public void maintain() {} + + @Override + public void setExportedInvoiceId(Bill.Id billId, String invoiceId) { + exportedInvoiceIds.put(billId, invoiceId); + } + + @Override + public void setExportedInvoiceItemId(String lineItemId, String invoiceItemId) { } + + private String exportedInvoiceId(Bill.Id billId) { + return exportedInvoiceIds.getOrDefault(billId, null); + } } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporter.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporter.java index 719d22429b8..676c29cec5d 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporter.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporter.java @@ -1,3 +1,4 @@ +// 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.api.integration.billing; import com.yahoo.vespa.hosted.controller.tenant.BillingReference; @@ -5,4 +6,12 @@ import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; public interface BillingReporter { BillingReference maintainTenant(CloudTenant tenant); + + InvoiceUpdate maintainInvoice(Bill bill); + + /** Export a bill to a payment service. Returns the invoice ID in the external system. */ + default String exportBill(Bill bill, String exportMethod, CloudTenant tenant) { + return "NOT_IMPLEMENTED"; + } + } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java index 34599f83a8c..689ecc356dc 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java @@ -1,21 +1,45 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import com.yahoo.vespa.hosted.controller.tenant.BillingReference; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; +import java.math.BigDecimal; import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.Optional; import java.util.UUID; public class BillingReporterMock implements BillingReporter { private final Clock clock; + private final BillingDatabaseClient dbClient; - public BillingReporterMock(Clock clock) { + public BillingReporterMock(Clock clock, BillingDatabaseClient dbClient) { this.clock = clock; + this.dbClient = dbClient; } @Override public BillingReference maintainTenant(CloudTenant tenant) { return new BillingReference(UUID.randomUUID().toString(), clock.instant()); } + + @Override + public InvoiceUpdate maintainInvoice(Bill bill) { + dbClient.addLineItem(bill.tenant(), maintainedMarkerItem(), Optional.of(bill.id())); + return new InvoiceUpdate(1,0,0); + } + + @Override + public String exportBill(Bill bill, String exportMethod, CloudTenant tenant) { + // Replace bill with a copy with exportedId set + var exportedId = "EXT-ID-123"; + dbClient.setExportedInvoiceId(bill.id(), exportedId); + return exportedId; + } + + private static Bill.LineItem maintainedMarkerItem() { + return new Bill.LineItem("maintained", "", BigDecimal.valueOf(0.0), "", "", ZonedDateTime.now()); + } + } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionMethod.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionMethod.java index 98abe883816..87c9a13d64a 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionMethod.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionMethod.java @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; public enum CollectionMethod { diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionResult.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionResult.java index 4cd040c7f11..4e83d90f6b0 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionResult.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionResult.java @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import java.util.Objects; @@ -52,4 +52,4 @@ public class CollectionResult { public int hashCode() { return Objects.hash(errorMessage); } -}
\ No newline at end of file +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java index e0a5ad9bdb7..ddcd5308986 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java @@ -1,10 +1,12 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.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; + /** * @author ogronnesby */ @@ -16,4 +18,15 @@ public interface CostCalculator { /** Estimate the cost for the given resources */ double calculate(NodeResources resources); + /** CPU unit price */ + BigDecimal getCpuPrice(); + + /** Memory unit price */ + BigDecimal getMemoryPrice(); + + /** Disk unit price */ + BigDecimal getDiskPrice(); + + /** GPU unit price */ + BigDecimal getGpuPrice(); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentList.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentList.java index 7e430bb18bb..559a3e8ee9c 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentList.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentList.java @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import java.util.ArrayList; diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentOwner.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentOwner.java index 8a333af0ddb..bc9f82c212b 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentOwner.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentOwner.java @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import com.yahoo.config.provision.TenantName; diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java new file mode 100644 index 00000000000..6ca3cf6ebb1 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java @@ -0,0 +1,45 @@ +package com.yahoo.vespa.hosted.controller.api.integration.billing; + +/** + * Helper to track changes to an invoice. + * + * @author gjoranv + */ +public record InvoiceUpdate(int itemsAdded, int itemsRemoved, int itemsModified) { + public boolean isEmpty() { + return itemsAdded == 0 && itemsRemoved == 0 && itemsModified == 0; + } + + public static InvoiceUpdate empty() { + return new InvoiceUpdate(0, 0, 0); + } + + public static class Counter { + private int itemsAdded = 0; + private int itemsRemoved = 0; + private int itemsModified = 0; + + public void addedItem() { + itemsAdded++; + } + + public void removedItem() { + itemsRemoved++; + } + + public void modifiedItem() { + itemsModified++; + } + + public void add(InvoiceUpdate other) { + itemsAdded += other.itemsAdded; + itemsRemoved += other.itemsRemoved; + itemsModified += other.itemsModified; + } + + public InvoiceUpdate finish() { + return new InvoiceUpdate(itemsAdded, itemsRemoved, itemsModified); + } + } + +} 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 eb20126304e..f8e83b44510 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 @@ -1,7 +1,8 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import java.math.BigDecimal; import java.time.Clock; @@ -22,18 +23,20 @@ import java.util.stream.Stream; public class MockBillingController implements BillingController { private final Clock clock; + private final BillingDatabaseClient dbClient; PlanId defaultPlan = PlanId.from("trial"); List<TenantName> tenants = new ArrayList<>(); Map<TenantName, PlanId> plans = new HashMap<>(); Map<TenantName, PaymentInstrument> activeInstruments = new HashMap<>(); Map<TenantName, List<Bill>> committedBills = new HashMap<>(); - Map<TenantName, Bill> uncommittedBills = new HashMap<>(); + public Map<TenantName, Bill> uncommittedBills = new HashMap<>(); Map<TenantName, List<Bill.LineItem>> unusedLineItems = new HashMap<>(); Map<TenantName, CollectionMethod> collectionMethod = new HashMap<>(); - public MockBillingController(Clock clock) { + public MockBillingController(Clock clock, BillingDatabaseClient dbClient) { this.clock = clock; + this.dbClient = dbClient; } @Override @@ -71,7 +74,7 @@ public class MockBillingController implements BillingController { .add(new Bill( billId, tenant, - Bill.StatusHistory.open(clock), + StatusHistory.open(clock), List.of(), startTime, endTime @@ -116,7 +119,7 @@ public class MockBillingController implements BillingController { } @Override - public void updateBillStatus(Bill.Id billId, String agent, String status) { + public void updateBillStatus(Bill.Id billId, String agent, BillStatus status) { var now = clock.instant().atZone(ZoneOffset.UTC); committedBills.values().stream() .flatMap(List::stream) @@ -134,7 +137,7 @@ public class MockBillingController implements BillingController { "line-item-id", description, amount, - "some-plan", + "paid", agent, ZonedDateTime.now())); } @@ -203,6 +206,7 @@ public class MockBillingController implements BillingController { return count < limit; } + public void setTenants(List<TenantName> tenants) { this.tenants = tenants; } @@ -234,6 +238,6 @@ public class MockBillingController implements BillingController { private Bill emptyBill() { var start = clock.instant().atZone(ZoneOffset.UTC); var end = clock.instant().atZone(ZoneOffset.UTC); - return new Bill(Bill.Id.of("empty"), TenantName.defaultName(), Bill.StatusHistory.open(clock), List.of(), start, end); + return new Bill(Bill.Id.of("empty"), TenantName.defaultName(), StatusHistory.open(clock), List.of(), start, end); } } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PaymentInstrument.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PaymentInstrument.java index 4d0f40afc2e..63771c3366a 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PaymentInstrument.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PaymentInstrument.java @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; /** diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Plan.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Plan.java index bd4c3c1a56f..5e964603db1 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Plan.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Plan.java @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; /** diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanId.java index 2ac0505d507..061f8731b41 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanId.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanId.java @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import java.util.Objects; 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 index 6b267e538e7..c0bd0dd29cd 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import java.util.List; @@ -20,6 +20,10 @@ public interface PlanRegistry { /** Get a set of all plans */ List<Plan> all(); + default Plan require(String planId) { + return plan(planId).orElseThrow(); + } + /** Get a plan give a plan ID */ default Optional<Plan> plan(String planId) { if (planId == null || planId.isBlank()) 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 index 89226d3309e..5af4d0cff29 100644 --- 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 @@ -1,3 +1,4 @@ +// 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.api.integration.billing; import com.yahoo.config.provision.NodeResources; @@ -134,7 +135,8 @@ public class PlanRegistryMock implements PlanRegistry { dgbCost, gpuCost, usage.getArchitecture(), - usage.getMajorVersion() + usage.getMajorVersion(), + usage.getCloudAccount() ); } @@ -142,5 +144,25 @@ public class PlanRegistryMock implements PlanRegistry { public double calculate(NodeResources resources) { return resources.cost(); } + + @Override + public BigDecimal getCpuPrice() { + return cpuHourCost; + } + + @Override + public BigDecimal getMemoryPrice() { + return memHourCost; + } + + @Override + public BigDecimal getDiskPrice() { + return dgbHourCost; + } + + @Override + public BigDecimal getGpuPrice() { + return gpuHourCost; + } } } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanResult.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanResult.java index b86d1199956..35e7fcbcf6e 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanResult.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanResult.java @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import java.util.Objects; diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java index 0859cc404ce..ca339f9cf15 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; import java.math.BigDecimal; diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/QuotaCalculator.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/QuotaCalculator.java index 0608e6d95ac..9511d1109ff 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/QuotaCalculator.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/QuotaCalculator.java @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.api.integration.billing; /** diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistory.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistory.java new file mode 100644 index 00000000000..f0c7f806c8c --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistory.java @@ -0,0 +1,61 @@ +package com.yahoo.vespa.hosted.controller.api.integration.billing; + +import java.time.Clock; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * @author ogronnesby + */ +public class StatusHistory { + SortedMap<ZonedDateTime, BillStatus> history; + + public StatusHistory(SortedMap<ZonedDateTime, BillStatus> history) { + // Validate the given history + var iter = history.values().iterator(); + BillStatus next = iter.hasNext() ? iter.next() : null; + while (iter.hasNext()) { + var current = next; + next = iter.next(); + if (! validateStatus(current, next)) { + throw new IllegalArgumentException("Invalid transition from " + current + " to " + next); + } + } + + this.history = history; + } + + public static StatusHistory open(Clock clock) { + var now = clock.instant().atZone(ZoneOffset.UTC); + return new StatusHistory( + new TreeMap<>(Map.of(now, BillStatus.OPEN)) + ); + } + + public BillStatus current() { + return history.get(history.lastKey()); + } + + public SortedMap<ZonedDateTime, BillStatus> getHistory() { + return history; + } + + public void checkValidTransition(BillStatus newStatus) { + if (! validateStatus(current(), newStatus)) { + throw new IllegalArgumentException("Invalid transition from " + current() + " to " + newStatus); + } + } + + private static boolean validateStatus(BillStatus current, BillStatus newStatus) { + return switch(current) { + case OPEN -> true; + case FROZEN -> newStatus != BillStatus.OPEN; // This could be subject to change. + case CLOSED -> newStatus == BillStatus.CLOSED; + case VOID -> newStatus == BillStatus.VOID; + }; + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/package-info.java index 35b1fc912f0..d05e9aa4c09 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/package-info.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/package-info.java @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. @ExportPackage package com.yahoo.vespa.hosted.controller.api.integration.billing; |