killbill-memoizeit
Changes
invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousInArrearUsageInterval.java 268(+194 -74)
invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionConsumableInArrear.java 139(+70 -69)
Details
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultUsage.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultUsage.java
index 2841609..bc75ed7 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultUsage.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultUsage.java
@@ -23,6 +23,7 @@ import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlID;
import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.BillingPeriod;
@@ -41,6 +42,10 @@ import org.killbill.billing.util.config.catalog.ValidationErrors;
public class DefaultUsage extends ValidatingConfig<StandaloneCatalog> implements Usage {
@XmlAttribute(required = true)
+ @XmlID
+ private String name;
+
+ @XmlAttribute(required = true)
private BillingMode billingMode;
@XmlAttribute(required = true)
@@ -77,6 +82,11 @@ public class DefaultUsage extends ValidatingConfig<StandaloneCatalog> implements
private PlanPhase phase;
@Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
public BillingMode getBillingMode() {
return billingMode;
}
@@ -171,6 +181,11 @@ public class DefaultUsage extends ValidatingConfig<StandaloneCatalog> implements
return this;
}
+ public DefaultUsage setName(final String name) {
+ this.name = name;
+ return this;
+ }
+
public DefaultUsage setBillingMode(final BillingMode billingMode) {
this.billingMode = billingMode;
return this;
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLReader.java b/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLReader.java
index 64cccf7..f05c686 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLReader.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLReader.java
@@ -67,6 +67,7 @@ public class TestXMLReader extends CatalogTestSuiteNoDB {
assertEquals(usages.length, 1);
final Usage usage = usages[0];
+ assertEquals(usage.getName(), "capacity-in-advance-monthly-usage1");
assertEquals(usage.getBillingPeriod(), BillingPeriod.MONTHLY);
assertEquals(usage.getUsageType(), UsageType.CAPACITY);
assertEquals(usage.getBillingMode(), BillingMode.IN_ADVANCE);
@@ -96,6 +97,7 @@ public class TestXMLReader extends CatalogTestSuiteNoDB {
assertEquals(usages.length, 1);
final Usage usage = usages[0];
+ assertEquals(usage.getName(), "consumable-in-advance-prepay-credit-monthly-usage1");
assertEquals(usage.getBillingPeriod(), BillingPeriod.MONTHLY);
assertEquals(usage.getUsageType(), UsageType.CONSUMABLE);
assertEquals(usage.getBillingMode(), BillingMode.IN_ADVANCE);
@@ -127,6 +129,7 @@ public class TestXMLReader extends CatalogTestSuiteNoDB {
assertEquals(usages.length, 1);
final Usage usage = usages[0];
+ assertEquals(usage.getName(), "consumable-in-advance-topup-usage1");
assertEquals(usage.getBillingPeriod(), BillingPeriod.NO_BILLING_PERIOD);
assertEquals(usage.getUsageType(), UsageType.CONSUMABLE);
assertEquals(usage.getBillingMode(), BillingMode.IN_ADVANCE);
@@ -161,6 +164,7 @@ public class TestXMLReader extends CatalogTestSuiteNoDB {
assertEquals(usages.length, 1);
final Usage usage = usages[0];
+ assertEquals(usage.getName(), "capacity-in-arrear-usage1");
assertEquals(usage.getBillingPeriod(), BillingPeriod.MONTHLY);
assertEquals(usage.getUsageType(), UsageType.CAPACITY);
assertEquals(usage.getBillingMode(), BillingMode.IN_ARREAR);
@@ -210,6 +214,7 @@ public class TestXMLReader extends CatalogTestSuiteNoDB {
assertEquals(usages.length, 1);
final Usage usage = usages[0];
+ assertEquals(usage.getName(), "consumable-in-arrear-usage1");
assertEquals(usage.getBillingPeriod(), BillingPeriod.MONTHLY);
assertEquals(usage.getUsageType(), UsageType.CONSUMABLE);
assertEquals(usage.getBillingMode(), BillingMode.IN_ARREAR);
diff --git a/catalog/src/test/resources/UsageExperimental.xml b/catalog/src/test/resources/UsageExperimental.xml
index cd18fd9..11f3d17 100644
--- a/catalog/src/test/resources/UsageExperimental.xml
+++ b/catalog/src/test/resources/UsageExperimental.xml
@@ -74,7 +74,7 @@
</duration>
<usages>
- <usage billingMode="IN_ADVANCE" usageType="CAPACITY">
+ <usage name="capacity-in-advance-monthly-usage1" billingMode="IN_ADVANCE" usageType="CAPACITY">
<billingPeriod>MONTHLY</billingPeriod>
<limits>
<limit>
@@ -102,7 +102,7 @@
<unit>UNLIMITED</unit>
</duration>
<usages>
- <usage billingMode="IN_ADVANCE" usageType="CONSUMABLE">
+ <usage name="consumable-in-advance-prepay-credit-monthly-usage1" billingMode="IN_ADVANCE" usageType="CONSUMABLE">
<billingPeriod>MONTHLY</billingPeriod>
<blocks>
<block>
@@ -135,7 +135,7 @@
</duration>
<usages>
- <usage billingMode="IN_ADVANCE" usageType="CONSUMABLE">
+ <usage name="consumable-in-advance-topup-usage1" billingMode="IN_ADVANCE" usageType="CONSUMABLE">
<billingPeriod>NO_BILLING_PERIOD</billingPeriod>
<blocks>
<block type="TOP_UP">
@@ -164,7 +164,7 @@
</duration>
<usages>
- <usage billingMode="IN_ARREAR" usageType="CAPACITY">
+ <usage name="capacity-in-arrear-usage1" billingMode="IN_ARREAR" usageType="CAPACITY">
<billingPeriod>MONTHLY</billingPeriod>
<tiers>
<tier>
@@ -229,7 +229,7 @@
<unit>UNLIMITED</unit>
</duration>
<usages>
- <usage billingMode="IN_ARREAR" usageType="CONSUMABLE">
+ <usage name="consumable-in-arrear-usage1" billingMode="IN_ARREAR" usageType="CONSUMABLE">
<billingPeriod>MONTHLY</billingPeriod>
<tiers>
<tier>
diff --git a/catalog/src/test/resources/WeaponsHireSmall.xml b/catalog/src/test/resources/WeaponsHireSmall.xml
index 3a686dd..c0501d3 100644
--- a/catalog/src/test/resources/WeaponsHireSmall.xml
+++ b/catalog/src/test/resources/WeaponsHireSmall.xml
@@ -122,7 +122,7 @@
</recurringPrice>
</recurring>
<usages>
- <usage billingMode="IN_ADVANCE" usageType="CAPACITY">
+ <usage name="usage1" billingMode="IN_ADVANCE" usageType="CAPACITY">
<billingPeriod>MONTHLY</billingPeriod>
<limits>
<limit>
@@ -226,7 +226,7 @@
</recurringPrice>
</recurring>
<usages>
- <usage billingMode="IN_ADVANCE" usageType="CAPACITY">
+ <usage name="usage2" billingMode="IN_ADVANCE" usageType="CAPACITY">
<billingPeriod>ANNUAL</billingPeriod>
<limits>
<limit>
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
index 3a57387..66af2e6 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
@@ -28,6 +28,7 @@ import javax.annotation.Nullable;
import org.joda.time.LocalDate;
import org.joda.time.Months;
import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.BillingPeriod;
@@ -51,6 +52,7 @@ import org.killbill.billing.junction.BillingEventSet;
import org.killbill.billing.usage.api.UsageUserApi;
import org.killbill.billing.util.config.InvoiceConfig;
import org.killbill.billing.util.currency.KillBillMoney;
+import org.killbill.billing.util.dao.NonEntityDao;
import org.killbill.clock.Clock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -69,12 +71,14 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
private final Clock clock;
private final InvoiceConfig config;
private final UsageUserApi usageApi;
+ private final NonEntityDao nonEntityDao;
@Inject
- public DefaultInvoiceGenerator(final Clock clock, final UsageUserApi usageApi, final InvoiceConfig config) {
+ public DefaultInvoiceGenerator(final Clock clock, final UsageUserApi usageApi, final InvoiceConfig config, final NonEntityDao nonEntityDao) {
this.clock = clock;
this.config = config;
this.usageApi = usageApi;
+ this.nonEntityDao = nonEntityDao;
}
/*
@@ -109,6 +113,7 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
@Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
final InternalCallContext context) throws InvoiceApiException {
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT);
try {
final List<InvoiceItem> items = Lists.newArrayList();
@@ -120,18 +125,13 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
final BillingEvent event = events.next();
final UUID subscriptionId = event.getSubscription().getId();
if (curSubscriptionId != null && !curSubscriptionId.equals(subscriptionId)) {
- //
- // STEPH_USAGE unitType issue
- // STEPH_USAGE context needs tenantId , hum...
- final UUID tenantId = UUID.randomUUID();
- SubscriptionConsumableInArrear foo = new SubscriptionConsumableInArrear(invoiceId, "foo", curEvents, usageApi, targetDate, context.toTenantContext(tenantId));
- items.addAll(foo.computeMissingUsageInvoiceItems(extractUsageItemsForSubscription(subscriptionId, existingInvoices)));
+ final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, usageApi, targetDate, context.toTenantContext(tenantId));
+ items.addAll(subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(extractUsageItemsForSubscription(subscriptionId, existingInvoices)));
curEvents.clear();
}
curSubscriptionId = subscriptionId;
curEvents.add(event);
}
-
return items;
} catch (CatalogApiException e) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemFactory.java b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemFactory.java
index de3bf99..a75c782 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemFactory.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemFactory.java
@@ -79,7 +79,7 @@ public class InvoiceItemFactory {
item = new ItemAdjInvoiceItem(id, createdDate, invoiceId, accountId, startDate, amount, currency, linkedItemId);
break;
case USAGE:
- // STEPH USAGE should we add unitType in schema pr override fields (planName,..) Same for unitAmount
+ // STEPH_USAGE should we add unitType in schema or override fields (planName,..) Same for unitAmount
item = new UsageInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, currency,"unitType");
break;
default:
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/UsageInvoiceItem.java b/invoice/src/main/java/org/killbill/billing/invoice/model/UsageInvoiceItem.java
index 9b6489e..f9d8892 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/UsageInvoiceItem.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/UsageInvoiceItem.java
@@ -28,19 +28,19 @@ import org.killbill.billing.invoice.api.InvoiceItemType;
public class UsageInvoiceItem extends InvoiceItemBase {
- private final String unitType;
+ private final String usageName;
public UsageInvoiceItem(final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId, @Nullable final UUID subscriptionId,
final String planName, final String phaseName,
- final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency, final String unitType) {
- this(UUID.randomUUID(), null, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, currency, unitType);
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency, final String usageName) {
+ this(UUID.randomUUID(), null, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, currency, usageName);
}
public UsageInvoiceItem(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, final UUID bundleId,
final UUID subscriptionId, final String planName, final String phaseName,
- final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency, final String unitType) {
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency, final String usageName) {
super(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, currency);
- this.unitType = unitType;
+ this.usageName = usageName;
}
@Override
@@ -50,10 +50,10 @@ public class UsageInvoiceItem extends InvoiceItemBase {
@Override
public String getDescription() {
- return String.format("%s (usage item)", unitType);
+ return String.format("%s (usage item)", usageName);
}
- public String getUnitType() {
- return unitType;
+ public String getUsageName() {
+ return usageName;
}
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousInArrearUsageInterval.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousInArrearUsageInterval.java
index 405306e..b05f6d9 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousInArrearUsageInterval.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousInArrearUsageInterval.java
@@ -17,7 +17,12 @@
package org.killbill.billing.invoice.usage;
import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.UUID;
import org.joda.time.DateTime;
@@ -44,24 +49,29 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import static org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear.getTieredBlocks;
+import static org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear.getUnitTypes;
import static org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear.localDateToEndOfDayInAccountTimezone;
+/**
+ * There is one such class per subscriptionId, matching a given in arrear/consumable usage section and
+ * referenced through a contiguous list of billing events.
+ */
public class ContiguousInArrearUsageInterval {
private final List<LocalDate> transitionTimes;
private final List<BillingEvent> billingEvents;
private final Usage usage;
- private final String unitType;
+ private final Set<String> unitTypes;
private final UsageUserApi usageApi;
private final LocalDate targetDate;
private final UUID invoiceId;
private final TenantContext context;
- public ContiguousInArrearUsageInterval(final Usage usage, final UUID invoiceId, final String unitType, final UsageUserApi usageApi, final LocalDate targetDate, final TenantContext context) {
+ public ContiguousInArrearUsageInterval(final Usage usage, final UUID invoiceId, final UsageUserApi usageApi, final LocalDate targetDate, final TenantContext context) {
this.usage = usage;
this.invoiceId = invoiceId;
- this.unitType = unitType;
+ this.unitTypes = getUnitTypes(usage);
this.usageApi = usageApi;
this.targetDate = targetDate;
this.context = context;
@@ -69,49 +79,16 @@ public class ContiguousInArrearUsageInterval {
this.transitionTimes = Lists.newLinkedList();
}
- public void addBillingEvent(final BillingEvent event) {
- billingEvents.add(event);
- }
-
- public Usage getUsage() {
- return usage;
- }
-
- public int getBCD() {
- return billingEvents.get(0).getBillCycleDayLocal();
- }
-
- public UUID getAccountId() {
- return billingEvents.get(0).getAccount().getId();
- }
-
- public UUID getBundleId() {
- return billingEvents.get(0).getSubscription().getBundleId();
- }
-
- public UUID getSubscriptionId() {
- return billingEvents.get(0).getSubscription().getId();
- }
-
- // STEPH_USAGE planName/phaseName,BCD,... might not be correct if we changed plan but Usage section was exactly similar
- public String getPlanName() {
- return billingEvents.get(0).getPlan().getName();
- }
-
- public String getPhaseName() {
- return billingEvents.get(0).getPlanPhase().getName();
- }
-
- public Currency getCurrency() {
- return billingEvents.get(0).getCurrency();
- }
-
- public DateTimeZone getAccountTimeZone() {
- return billingEvents.get(0).getTimeZone();
- }
-
-
-
+ /**
+ * Builds the transitionTimes associated to that usage section. Those are determined based on billing events for when to start and when to stop,
+ * the per usage billingPeriod and finally the targetDate.
+ * <p/>
+ * Those transition dates define the well defined billing granularity periods that should be billed for that specific usage section.
+ *
+ * @param closedInterval whether there was a last billing event referencing the usage section or whether this is ongoing and
+ * then targetDate will define the endDate.
+ * @return
+ */
public ContiguousInArrearUsageInterval build(final boolean closedInterval) {
Preconditions.checkState((!closedInterval && billingEvents.size() >= 1) ||
@@ -140,26 +117,38 @@ public class ContiguousInArrearUsageInterval {
return this;
}
+ /**
+ * Compute the missing usage invoice items based on what should be billed and what has been billed ($ amount comparison).
+ *
+ * @param existingUsage existing on disk usage items for the subscription
+ * @return
+ * @throws CatalogApiException
+ */
public List<InvoiceItem> computeMissingItems(final List<InvoiceItem> existingUsage) throws CatalogApiException {
final List<InvoiceItem> result = Lists.newLinkedList();
- final List<RolledUpUsage> rolledUpUsages = getRolledUpUsage();
- for (RolledUpUsage ru : rolledUpUsages) {
- final LocalDate startRolledUpDate = new LocalDate(ru.getStartTime(), getAccountTimeZone());
- final LocalDate endRolledUpDate = new LocalDate(ru.getEndTime(), getAccountTimeZone());
- final BigDecimal billedUsage = computeBilledUsage(startRolledUpDate, endRolledUpDate, existingUsage);
- final BigDecimal toBeBilledUsage = computeToBeBilledUsage(ru.getAmount());
- if (billedUsage.compareTo(toBeBilledUsage) < 0) {
- InvoiceItem item = new UsageInvoiceItem(invoiceId, getAccountId(), getBundleId(), getSubscriptionId(), getPlanName(),
- getPhaseName(), startRolledUpDate, endRolledUpDate, toBeBilledUsage.subtract(billedUsage), getCurrency(), unitType);
- result.add(item);
+ final RolledUpUsageForUnitTypesFactory factory = new RolledUpUsageForUnitTypesFactory(getRolledUpUsage(), unitTypes, getAccountTimeZone());
+ for (RolledUpUsageForUnitTypes ru : factory.getOrderedRolledUpUsageForUnitTypes()) {
+
+ final BigDecimal billedUsage = computeBilledUsage(ru.getStartDate(), ru.getEndDate(), existingUsage);
+ final BigDecimal toBeBilledUsage = BigDecimal.ZERO;
+ for (final String unitType : unitTypes) {
+ final BigDecimal usageAmountForUnitType = ru.getUsageAmountForUnitType(unitType);
+ final BigDecimal toBeBilledForUnit = computeToBeBilledUsage(usageAmountForUnitType, unitType);
+ toBeBilledUsage.add(toBeBilledForUnit);
+
+ if (billedUsage.compareTo(toBeBilledUsage) < 0) {
+ InvoiceItem item = new UsageInvoiceItem(invoiceId, getAccountId(), getBundleId(), getSubscriptionId(), getPlanName(),
+ getPhaseName(), ru.getStartDate(), ru.getEndDate(), toBeBilledUsage.subtract(billedUsage), getCurrency(), usage.getName());
+ result.add(item);
+ }
}
}
return result;
}
- private List<RolledUpUsage> getRolledUpUsage() {
+ List<RolledUpUsage> getRolledUpUsage() {
final Iterable<DateTime> transitions = Iterables.transform(transitionTimes, new Function<LocalDate, DateTime>() {
@Override
@@ -167,36 +156,46 @@ public class ContiguousInArrearUsageInterval {
return localDateToEndOfDayInAccountTimezone(input, getAccountTimeZone());
}
});
- final List<RolledUpUsage> usagesForInterval = usageApi.getAllUsageForSubscription(getSubscriptionId(), unitType, ImmutableList.copyOf(transitions), context);
- return usagesForInterval;
+ // STEPH_USAGE optimized api takes set of unitTypes -- for usage section-- and list of transitions date. Should we use dateTime or LocalDate?
+ return usageApi.getAllUsageForSubscription(getSubscriptionId(), unitTypes, ImmutableList.copyOf(transitions), context);
}
- private final BigDecimal computeToBeBilledUsage(final BigDecimal units) throws CatalogApiException {
+ BigDecimal computeToBeBilledUsage(final BigDecimal nbUnits, final String unitType) throws CatalogApiException {
- // STEPH_USAGE need to review catalog xml which defines block tiers, ...
- final int blockSize = 0x1000;
- final int nbBlocks = units.intValue() / blockSize + ((units.intValue() % blockSize == 0) ? 0 : 1);
+ BigDecimal result = BigDecimal.ZERO;
- // STEPH_USAGE this is wrong should use from each tier.
final List<TieredBlock> tieredBlocks = getTieredBlocks(usage, unitType);
- for (TieredBlock tier : tieredBlocks) {
- if (tier.getMax() >= units.doubleValue()) {
- return tier.getPrice().getPrice(getCurrency());
+ int remainingUnits = nbUnits.intValue();
+ for (TieredBlock tieredBlock : tieredBlocks) {
+
+ final int blockTierSize = tieredBlock.getSize().intValue();
+ final int tmp = remainingUnits / blockTierSize + (remainingUnits % blockTierSize == 0 ? 0 : 1);
+ final int nbUsedTierBlocks;
+ if (tmp > tieredBlock.getMax()) {
+ nbUsedTierBlocks = tieredBlock.getMax().intValue();
+ remainingUnits -= tieredBlock.getMax() * blockTierSize;
+ } else {
+ nbUsedTierBlocks = tmp;
+ remainingUnits = 0;
}
+ result.add(tieredBlock.getPrice().getPrice(getCurrency()).multiply(new BigDecimal(nbUsedTierBlocks)));
}
- // Return from last tier
- return tieredBlocks.get(tieredBlocks.size() - 1).getPrice().getPrice(getCurrency());
+ return result;
}
- private final BigDecimal computeBilledUsage(final LocalDate startDate, final LocalDate endDate, final List<InvoiceItem> existingUsage) {
+ BigDecimal computeBilledUsage(final LocalDate startDate, final LocalDate endDate, final List<InvoiceItem> existingUsage) {
final Iterable<InvoiceItem> filteredUsageForInterval = Iterables.filter(existingUsage, new Predicate<InvoiceItem>() {
@Override
public boolean apply(final InvoiceItem input) {
+ if (input.getInvoiceItemType() != InvoiceItemType.USAGE) {
+ return false;
+ }
+
// STEPH_USAGE what happens if we discover usage period that overlap (one side or both side) the [startDate, endDate] interval
- // STEPH_USAGE how to distinguish different usage charges (maybe different sections.) (needs to at least of the unitType in usage element
- return input.getInvoiceItemType() == InvoiceItemType.USAGE &&
- input.getStartDate().compareTo(startDate) >= 0 &&
- input.getEndDate().compareTo(endDate) <= 0;
+ final UsageInvoiceItem usageInput = (UsageInvoiceItem) input;
+ return usageInput.getUsageName().equals(usage.getName()) &&
+ usageInput.getStartDate().compareTo(startDate) >= 0 &&
+ usageInput.getEndDate().compareTo(endDate) <= 0;
}
});
@@ -208,4 +207,125 @@ public class ContiguousInArrearUsageInterval {
return billedAmount;
}
+ private static class RolledUpUsageForUnitTypesFactory {
+
+ private final Map<String, RolledUpUsageForUnitTypes> map;
+
+ public RolledUpUsageForUnitTypesFactory(final List<RolledUpUsage> rolledUpUsages, final Set<String> unitTypes, final DateTimeZone accountTimeZone) {
+ map = new HashMap<String, RolledUpUsageForUnitTypes>();
+ for (RolledUpUsage ru : rolledUpUsages) {
+
+ final LocalDate startRolledUpDate = new LocalDate(ru.getStartTime(), accountTimeZone);
+ final LocalDate endRolledUpDate = new LocalDate(ru.getEndTime(), accountTimeZone);
+ final String key = startRolledUpDate + "-" + endRolledUpDate;
+
+ RolledUpUsageForUnitTypes usageForUnitTypes = map.get(key);
+ if (usageForUnitTypes == null) {
+ usageForUnitTypes = new RolledUpUsageForUnitTypes(startRolledUpDate, endRolledUpDate, unitTypes);
+ map.put(key, usageForUnitTypes);
+ }
+ usageForUnitTypes.addUsageForUnit(ru.getUnitType(), ru.getAmount());
+ }
+ }
+
+ public List<RolledUpUsageForUnitTypes> getOrderedRolledUpUsageForUnitTypes() {
+ final LinkedList<RolledUpUsageForUnitTypes> result = new LinkedList<RolledUpUsageForUnitTypes>(map.values());
+ Collections.sort(result);
+ return result;
+ }
+ }
+
+ /**
+ * Internal classes to transform RolledUpUsage into a map of usage (types, amount) across each billable interval.
+ */
+ private static class RolledUpUsageForUnitTypes implements Comparable {
+
+ private final LocalDate startDate;
+ private final LocalDate endDate;
+ private final Map<String, BigDecimal> unitAmounts;
+
+ private RolledUpUsageForUnitTypes(final LocalDate endDate, final LocalDate startDate, final Set<String> unitTypes) {
+ this.endDate = endDate;
+ this.startDate = startDate;
+ this.unitAmounts = new HashMap<String, BigDecimal>();
+ for (final String type : unitTypes) {
+ unitAmounts.put(type, BigDecimal.ZERO);
+ }
+ }
+
+ public void addUsageForUnit(final String unitType, BigDecimal amount) {
+ final BigDecimal currentAmount = unitAmounts.get(unitType);
+ unitAmounts.put(unitType, currentAmount.add(amount));
+ }
+
+ public LocalDate getStartDate() {
+ return startDate;
+ }
+
+ public LocalDate getEndDate() {
+ return endDate;
+ }
+
+ public BigDecimal getUsageAmountForUnitType(final String unitType) {
+ return unitAmounts.get(unitType);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = startDate != null ? startDate.hashCode() : 0;
+ result = 31 * result + (endDate != null ? endDate.hashCode() : 0);
+ result = 31 * result + (unitAmounts != null ? unitAmounts.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public int compareTo(final Object o) {
+
+ Preconditions.checkArgument(o instanceof RolledUpUsageForUnitTypes);
+ final RolledUpUsageForUnitTypes other = (RolledUpUsageForUnitTypes) o;
+ // We will check later intervals don't overlap.
+ return getEndDate().compareTo(other.getStartDate());
+ }
+ }
+
+ public void addBillingEvent(final BillingEvent event) {
+ billingEvents.add(event);
+ }
+
+ public Usage getUsage() {
+ return usage;
+ }
+
+ public int getBCD() {
+ return billingEvents.get(0).getBillCycleDayLocal();
+ }
+
+ public UUID getAccountId() {
+ return billingEvents.get(0).getAccount().getId();
+ }
+
+ public UUID getBundleId() {
+ return billingEvents.get(0).getSubscription().getBundleId();
+ }
+
+ public UUID getSubscriptionId() {
+ return billingEvents.get(0).getSubscription().getId();
+ }
+
+ // STEPH_USAGE planName/phaseName,BCD,... might not be correct if we changed plan but Usage section was exactly similar
+ public String getPlanName() {
+ return billingEvents.get(0).getPlan().getName();
+ }
+
+ public String getPhaseName() {
+ return billingEvents.get(0).getPlanPhase().getName();
+ }
+
+ public Currency getCurrency() {
+ return billingEvents.get(0).getCurrency();
+ }
+
+ public DateTimeZone getAccountTimeZone() {
+ return billingEvents.get(0).getTimeZone();
+ }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionConsumableInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionConsumableInArrear.java
index 14f932a..6736857 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionConsumableInArrear.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionConsumableInArrear.java
@@ -17,8 +17,10 @@
package org.killbill.billing.invoice.usage;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -40,127 +42,126 @@ import org.killbill.billing.util.callcontext.TenantContext;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
+/**
+ * There is one such class created for each subscriptionId referenced in the billingEvents.
+ *
+ */
public class SubscriptionConsumableInArrear {
- private UUID invoiceId;
- private final String unitType;
+ private final UUID invoiceId;
private final List<BillingEvent> subscriptionBillingEvents;
private final UsageUserApi usageApi;
private final LocalDate targetDate;
private final TenantContext context;
- public SubscriptionConsumableInArrear(final UUID invoiceId, final String unitType, final List<BillingEvent> subscriptionBillingEvents, final UsageUserApi usageApi, final LocalDate targetDate, final TenantContext context) {
+ public SubscriptionConsumableInArrear(final UUID invoiceId, final List<BillingEvent> subscriptionBillingEvents, final UsageUserApi usageApi, final LocalDate targetDate, final TenantContext context) {
this.invoiceId = invoiceId;
- this.unitType = unitType;
this.subscriptionBillingEvents = subscriptionBillingEvents;
this.usageApi = usageApi;
this.targetDate = targetDate;
this.context = context;
}
+ /**
+ * Based on billing events, (@code existingUsage} and targetDate, figure out what remains to be billed.
+ *
+ * @param existingUsage the existing on disk usage items.
+ * @return
+ * @throws CatalogApiException
+ */
public List<InvoiceItem> computeMissingUsageInvoiceItems(final List<InvoiceItem> existingUsage) throws CatalogApiException {
final List<InvoiceItem> result = Lists.newLinkedList();
- final List<ContiguousInArrearUsageInterval> billingEventTransitionTimePeriods = computeBillingEventTransitionTimePeriods();
+ final List<ContiguousInArrearUsageInterval> billingEventTransitionTimePeriods = computeInArrearUsageInterval();
for (ContiguousInArrearUsageInterval usageInterval : billingEventTransitionTimePeriods) {
result.addAll(usageInterval.computeMissingItems(existingUsage));
}
return result;
}
+ List<ContiguousInArrearUsageInterval> computeInArrearUsageInterval() {
- static List<TieredBlock> getTieredBlocks(final Usage usage, final String unitType) {
-
- Preconditions.checkArgument(usage.getTiers().length > 0);
-
- final List<TieredBlock> result = Lists.newLinkedList();
- for (Tier tier : usage.getTiers()) {
-
- for (TieredBlock tierBlock : tier.getTieredBlocks()) {
- if (tierBlock.getUnit().getName().equals(unitType)) {
- result.add(tierBlock);
- }
- }
- }
- return result;
- }
-
- static DateTime localDateToEndOfDayInAccountTimezone(final LocalDate input, final DateTimeZone accountTimeZone) {
- final DateTime dateTimeInAccountTimeZone = new DateTime(input.getYear(), input.getMonthOfYear(), input.getDayOfMonth(), 23, 59, 59, accountTimeZone);
- return new DateTime(dateTimeInAccountTimeZone, DateTimeZone.UTC);
- }
+ final List<ContiguousInArrearUsageInterval> usageIntervals = Lists.newLinkedList();
- private List<ContiguousInArrearUsageInterval> computeBillingEventTransitionTimePeriods() {
+ final Map<String, ContiguousInArrearUsageInterval> inFlightInArrearUsageIntervals = new HashMap<String, ContiguousInArrearUsageInterval>();
+ for (BillingEvent event : subscriptionBillingEvents) {
- final List<ContiguousInArrearUsageInterval> usageInterval = Lists.newLinkedList();
+ // All inflight usage interval are candidates to be closed unless we see that current billing event referencing the same usage section.
+ final Set<String> toBeClosed = inFlightInArrearUsageIntervals.keySet();
- ContiguousInArrearUsageInterval existingInterval = null;
- for (BillingEvent event : subscriptionBillingEvents) {
- final Usage usage = findUsage(event);
- if (usage == null || !usage.equals(existingInterval.getUsage())) {
- if (existingInterval != null) {
- usageInterval.add(existingInterval.build(true));
- existingInterval = null;
- }
- }
+ // Extract all in arrear /consumable usage section for that billing event.
+ final List<Usage> usages = findConsumableInArrearUsages(event);
+ for (Usage usage : usages) {
- if (usage != null) {
+ // Add inflight usage interval if non existent
+ ContiguousInArrearUsageInterval existingInterval = inFlightInArrearUsageIntervals.get(usage.getName());
if (existingInterval == null) {
- existingInterval = new ContiguousInArrearUsageInterval(usage, invoiceId, unitType, usageApi, targetDate, context);
+ existingInterval = new ContiguousInArrearUsageInterval(usage, invoiceId, usageApi, targetDate, context);
+ inFlightInArrearUsageIntervals.put(usage.getName(), existingInterval);
}
+ // Add billing event for that usage interval
existingInterval.addBillingEvent(event);
+ // Remove usage interval for toBeClosed set
+ toBeClosed.remove(usage.getName());
+ }
+
+ // Build the usage interval that are no longer referenced
+ for (String usageName : toBeClosed) {
+ usageIntervals.add(inFlightInArrearUsageIntervals.remove(usageName).build(true));
}
}
- if (existingInterval != null) {
- usageInterval.add(existingInterval.build(false));
+ for (String usageName : inFlightInArrearUsageIntervals.keySet()) {
+ usageIntervals.add(inFlightInArrearUsageIntervals.remove(usageName).build(false));
}
- return usageInterval;
+ return usageIntervals;
}
- private Usage findUsage(final BillingEvent event) {
+ List<Usage> findConsumableInArrearUsages(final BillingEvent event) {
if (event.getUsages().size() == 0) {
- return null;
+ return Collections.emptyList();
}
+
+ final List<Usage> result = Lists.newArrayList();
for (Usage usage : event.getUsages()) {
if (usage.getUsageType() != UsageType.CONSUMABLE ||
usage.getBillingMode() != BillingMode.IN_ARREAR) {
continue;
}
-
- List<TieredBlock> tieredBlock = getTieredBlocks(usage, unitType);
- if (tieredBlock.size() > 0) {
- return usage;
- }
+ result.add(usage);
}
- return null;
+ return result;
}
- private void addMissingTransitionTimes(final List<LocalDate> transitionTimes, final List<UsageInvoiceItem> existingUsage) {
-
- Preconditions.checkArgument(transitionTimes.size() > 0);
+ static List<TieredBlock> getTieredBlocks(final Usage usage, final String unitType) {
- final LocalDate startDate = transitionTimes.get(0);
- final LocalDate endDate = transitionTimes.get(transitionTimes.size() - 1);
+ Preconditions.checkArgument(usage.getTiers().length > 0);
- for (UsageInvoiceItem ii : existingUsage) {
- if (ii.getEndDate().compareTo(startDate) <= 0 || ii.getStartDate().compareTo(endDate) >= 0) {
- continue;
- }
- if (ii.getStartDate().compareTo(startDate) < 0 && ii.getEndDate().compareTo(endDate) <= 0) {
- transitionTimes.add(ii.getEndDate());
- } else if (ii.getStartDate().compareTo(startDate) >= 0 && ii.getEndDate().compareTo(endDate) > 0) {
- transitionTimes.add(ii.getStartDate());
- } else {
- transitionTimes.add(ii.getStartDate());
- transitionTimes.add(ii.getEndDate());
+ final List<TieredBlock> result = Lists.newLinkedList();
+ for (Tier tier : usage.getTiers()) {
+ for (TieredBlock tierBlock : tier.getTieredBlocks()) {
+ if (tierBlock.getUnit().getName().equals(unitType)) {
+ result.add(tierBlock);
+ }
}
}
+ return result;
+ }
- final Set<LocalDate> uniqueTransitions = new HashSet<LocalDate>(transitionTimes);
- transitionTimes.clear();
- transitionTimes.addAll(uniqueTransitions);
- Collections.sort(transitionTimes);
+ static DateTime localDateToEndOfDayInAccountTimezone(final LocalDate input, final DateTimeZone accountTimeZone) {
+ final DateTime dateTimeInAccountTimeZone = new DateTime(input.getYear(), input.getMonthOfYear(), input.getDayOfMonth(), 23, 59, 59, accountTimeZone);
+ return new DateTime(dateTimeInAccountTimeZone, DateTimeZone.UTC);
}
+ static Set<String> getUnitTypes(final Usage usage) {
+ Preconditions.checkArgument(usage.getTiers().length > 0);
+
+ final Set<String> result = new HashSet<String>();
+ for (Tier tier : usage.getTiers()) {
+ for (TieredBlock tierBlock : tier.getTieredBlocks()) {
+ result.add(tierBlock.getUnit().getName());
+ }
+ }
+ return result;
+ }
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index 20879e6..faa4eb2 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -102,7 +102,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
return false;
}
};
- this.generator = new DefaultInvoiceGenerator(clock, null, invoiceConfig);
+ this.generator = new DefaultInvoiceGenerator(clock, null, invoiceConfig, null);
}
@Test(groups = "fast")
diff --git a/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultUsageUserApi.java b/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultUsageUserApi.java
index c917a59..112bb60 100644
--- a/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultUsageUserApi.java
+++ b/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultUsageUserApi.java
@@ -18,6 +18,7 @@ package org.killbill.billing.usage.api.user;
import java.math.BigDecimal;
import java.util.List;
+import java.util.Set;
import java.util.UUID;
import javax.inject.Inject;
@@ -60,8 +61,8 @@ public class DefaultUsageUserApi implements UsageUserApi {
}
@Override
- public List<RolledUpUsage> getAllUsageForSubscription(final UUID subscriptionId, final String unitType, final List<DateTime> transitionTimes, final TenantContext tenantContext) {
- // STEPH USAGE
+ public List<RolledUpUsage> getAllUsageForSubscription(final UUID subscriptionId, final Set<String> unitTypes, final List<DateTime> transitionTimes, final TenantContext tenantContext) {
+ // STEPH_USAGE
return null;
}
}