killbill-memoizeit
Changes
invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java 12(+8 -4)
invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java 6(+6 -0)
invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java 227(+210 -17)
invoice/src/main/resources/org/killbill/billing/invoice/migration/V20180123114605__invoice_item_quantity_item_details.sql 2(+2 -0)
invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalCapacityInArrear.java 164(+150 -14)
invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java 426(+408 -18)
invoice/src/test/java/org/killbill/billing/invoice/usage/TestSubscriptionConsumableInArrear.java 2(+1 -1)
pom.xml 2(+1 -1)
Details
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index 07a8c84..c3b2036 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -1049,6 +1049,16 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
return isInvoicingSystemEnabled();
}
+ @Override
+ public UsageDetailMode getItemResultBehaviorMode() {
+ return defaultInvoiceConfig.getItemResultBehaviorMode();
+ }
+
+ @Override
+ public UsageDetailMode getItemResultBehaviorMode(final InternalTenantContext tenantContext) {
+ return getItemResultBehaviorMode();
+ }
+
public void setInvoicingSystemEnabled(final boolean invoicingSystemEnabled) {
isInvoicingSystemEnabled = invoicingSystemEnabled;
}
diff --git a/catalog/src/test/resources/UsageExperimental.xml b/catalog/src/test/resources/UsageExperimental.xml
index 02d2a50..4f9c732 100644
--- a/catalog/src/test/resources/UsageExperimental.xml
+++ b/catalog/src/test/resources/UsageExperimental.xml
@@ -242,7 +242,7 @@
<unit>UNLIMITED</unit>
</duration>
<usages>
- <usage name="consumable-in-arrear-usage1" billingMode="IN_ARREAR" usageType="CONSUMABLE">
+ <usage name="consumable-in-arrear-usage1" billingMode="IN_ARREAR" usageType="CONSUMABLE" tierBlockPolicy="ALL_TIERS">
<billingPeriod>MONTHLY</billingPeriod>
<tiers>
<tier>
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java b/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
index 70f9c13..ea76847 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
@@ -32,6 +32,7 @@ import org.skife.config.TimeSpan;
public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements InvoiceConfig {
+
private final InvoiceConfig staticConfig;
@Inject
@@ -163,6 +164,31 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
}
@Override
+ public UsageDetailMode getItemResultBehaviorMode() {
+ final UsageDetailMode mode = staticConfig.getItemResultBehaviorMode();
+ if (mode == UsageDetailMode.AGGREGATE || mode == UsageDetailMode.DETAIL) {
+ return mode;
+ }
+
+ return UsageDetailMode.AGGREGATE;
+ }
+
+ @Override
+ public UsageDetailMode getItemResultBehaviorMode(final InternalTenantContext tenantContext) {
+ final UsageDetailMode mode = staticConfig.getItemResultBehaviorMode();
+ final String result = getStringTenantConfig("getItemResultBehaviorMode", tenantContext);
+ if (result != null){
+ return UsageDetailMode.valueOf(result);
+ }
+
+ if (mode == UsageDetailMode.AGGREGATE || mode == UsageDetailMode.DETAIL) {
+ return mode;
+ }
+
+ return UsageDetailMode.AGGREGATE;
+ }
+
+ @Override
protected Class<? extends KillbillConfig> getConfigClass() {
return InvoiceConfig.class;
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemModelDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemModelDao.java
index e814a69..8104474 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemModelDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemModelDao.java
@@ -47,13 +47,15 @@ public class InvoiceItemModelDao extends EntityModelDaoBase implements EntityMod
private BigDecimal rate;
private Currency currency;
private UUID linkedItemId;
+ private Integer quantity;
+ private String itemDetails;
public InvoiceItemModelDao() { /* For the DAO mapper */ }
public InvoiceItemModelDao(final UUID id, final DateTime createdDate, final InvoiceItemType type, final UUID invoiceId, final UUID accountId,
final UUID childAccountId, final UUID bundleId, final UUID subscriptionId, final String description, final String planName,
final String phaseName, final String usageName, final LocalDate startDate, final LocalDate endDate, final BigDecimal amount,
- final BigDecimal rate, final Currency currency, final UUID linkedItemId) {
+ final BigDecimal rate, final Currency currency, final UUID linkedItemId, final Integer quantity, final String itemDetails) {
super(id, createdDate, createdDate);
this.type = type;
this.invoiceId = invoiceId;
@@ -71,6 +73,25 @@ public class InvoiceItemModelDao extends EntityModelDaoBase implements EntityMod
this.rate = rate;
this.currency = currency;
this.linkedItemId = linkedItemId;
+ this.quantity = quantity;
+ this.itemDetails = itemDetails;
+ }
+
+ public InvoiceItemModelDao(final UUID id, final DateTime createdDate, final InvoiceItemType type, final UUID invoiceId, final UUID accountId,
+ final UUID childAccountId, final UUID bundleId, final UUID subscriptionId, final String description, final String planName,
+ final String phaseName, final String usageName, final LocalDate startDate, final LocalDate endDate, final BigDecimal amount,
+ final BigDecimal rate, final Currency currency, final UUID linkedItemId) {
+ this(id, createdDate, type, invoiceId, accountId, childAccountId, bundleId, subscriptionId, description, planName, phaseName, usageName,
+ startDate, endDate, amount, rate, currency, linkedItemId, null, null);
+
+ }
+
+ public InvoiceItemModelDao(final DateTime createdDate, final InvoiceItemType type, final UUID invoiceId, final UUID accountId,
+ final UUID bundleId, final UUID subscriptionId, final String description, final String planName,
+ final String phaseName, final String usageName, final LocalDate startDate, final LocalDate endDate, final BigDecimal amount,
+ final BigDecimal rate, final Currency currency, final UUID linkedItemId, final Integer quantity, final String itemDetails) {
+ this(UUIDs.randomUUID(), createdDate, type, invoiceId, accountId, null, bundleId, subscriptionId, description, planName, phaseName, usageName,
+ startDate, endDate, amount, rate, currency, linkedItemId, quantity, itemDetails);
}
public InvoiceItemModelDao(final DateTime createdDate, final InvoiceItemType type, final UUID invoiceId, final UUID accountId,
@@ -78,13 +99,13 @@ public class InvoiceItemModelDao extends EntityModelDaoBase implements EntityMod
final String phaseName, final String usageName, final LocalDate startDate, final LocalDate endDate, final BigDecimal amount,
final BigDecimal rate, final Currency currency, final UUID linkedItemId) {
this(UUIDs.randomUUID(), createdDate, type, invoiceId, accountId, null, bundleId, subscriptionId, description, planName, phaseName, usageName,
- startDate, endDate, amount, rate, currency, linkedItemId);
+ startDate, endDate, amount, rate, currency, linkedItemId, null, null);
}
public InvoiceItemModelDao(final InvoiceItem invoiceItem) {
this(invoiceItem.getId(), invoiceItem.getCreatedDate(), invoiceItem.getInvoiceItemType(), invoiceItem.getInvoiceId(), invoiceItem.getAccountId(), invoiceItem.getChildAccountId(), invoiceItem.getBundleId(),
invoiceItem.getSubscriptionId(), invoiceItem.getDescription(), invoiceItem.getPlanName(), invoiceItem.getPhaseName(), invoiceItem.getUsageName(), invoiceItem.getStartDate(), invoiceItem.getEndDate(),
- invoiceItem.getAmount(), invoiceItem.getRate(), invoiceItem.getCurrency(), invoiceItem.getLinkedItemId());
+ invoiceItem.getAmount(), invoiceItem.getRate(), invoiceItem.getCurrency(), invoiceItem.getLinkedItemId(), invoiceItem.getQuantity(), invoiceItem.getItemDetails());
}
/*
@@ -99,128 +120,144 @@ public class InvoiceItemModelDao extends EntityModelDaoBase implements EntityMod
return type;
}
+ public void setType(final InvoiceItemType type) {
+ this.type = type;
+ }
+
public UUID getInvoiceId() {
return invoiceId;
}
+ public void setInvoiceId(final UUID invoiceId) {
+ this.invoiceId = invoiceId;
+ }
+
public UUID getAccountId() {
return accountId;
}
+ public void setAccountId(final UUID accountId) {
+ this.accountId = accountId;
+ }
+
public UUID getChildAccountId() {
return childAccountId;
}
+ public void setChildAccountId(final UUID childAccountId) {
+ this.childAccountId = childAccountId;
+ }
+
public UUID getBundleId() {
return bundleId;
}
- public UUID getSubscriptionId() {
- return subscriptionId;
+ public void setBundleId(final UUID bundleId) {
+ this.bundleId = bundleId;
}
- public String getDescription() {
- return description;
+ public UUID getSubscriptionId() {
+ return subscriptionId;
}
- public String getPlanName() {
- return planName;
+ public void setSubscriptionId(final UUID subscriptionId) {
+ this.subscriptionId = subscriptionId;
}
- public String getPhaseName() {
- return phaseName;
+ public String getDescription() {
+ return description;
}
- public String getUsageName() {
- return usageName;
+ public void setDescription(final String description) {
+ this.description = description;
}
- public LocalDate getStartDate() {
- return startDate;
+ public String getPlanName() {
+ return planName;
}
- public LocalDate getEndDate() {
- return endDate;
+ public void setPlanName(final String planName) {
+ this.planName = planName;
}
- public BigDecimal getAmount() {
- return amount;
+ public String getPhaseName() {
+ return phaseName;
}
- public BigDecimal getRate() {
- return rate;
+ public void setPhaseName(final String phaseName) {
+ this.phaseName = phaseName;
}
- public Currency getCurrency() {
- return currency;
+ public String getUsageName() {
+ return usageName;
}
- public UUID getLinkedItemId() {
- return linkedItemId;
+ public void setUsageName(final String usageName) {
+ this.usageName = usageName;
}
- public void setType(final InvoiceItemType type) {
- this.type = type;
+ public LocalDate getStartDate() {
+ return startDate;
}
- public void setInvoiceId(final UUID invoiceId) {
- this.invoiceId = invoiceId;
+ public void setStartDate(final LocalDate startDate) {
+ this.startDate = startDate;
}
- public void setAccountId(final UUID accountId) {
- this.accountId = accountId;
+ public LocalDate getEndDate() {
+ return endDate;
}
- public void setChildAccountId(final UUID childAccountId) {
- this.childAccountId = childAccountId;
+ public void setEndDate(final LocalDate endDate) {
+ this.endDate = endDate;
}
- public void setBundleId(final UUID bundleId) {
- this.bundleId = bundleId;
+ public BigDecimal getAmount() {
+ return amount;
}
- public void setSubscriptionId(final UUID subscriptionId) {
- this.subscriptionId = subscriptionId;
+ public void setAmount(final BigDecimal amount) {
+ this.amount = amount;
}
- public void setDescription(final String description) {
- this.description = description;
+ public BigDecimal getRate() {
+ return rate;
}
- public void setPlanName(final String planName) {
- this.planName = planName;
+ public void setRate(final BigDecimal rate) {
+ this.rate = rate;
}
- public void setPhaseName(final String phaseName) {
- this.phaseName = phaseName;
+ public Currency getCurrency() {
+ return currency;
}
- public void setUsageName(final String usageName) {
- this.usageName = usageName;
+ public void setCurrency(final Currency currency) {
+ this.currency = currency;
}
- public void setStartDate(final LocalDate startDate) {
- this.startDate = startDate;
+ public UUID getLinkedItemId() {
+ return linkedItemId;
}
- public void setEndDate(final LocalDate endDate) {
- this.endDate = endDate;
+ public void setLinkedItemId(final UUID linkedItemId) {
+ this.linkedItemId = linkedItemId;
}
- public void setAmount(final BigDecimal amount) {
- this.amount = amount;
+ public Integer getQuantity() {
+ return quantity;
}
- public void setRate(final BigDecimal rate) {
- this.rate = rate;
+ public void setQuantity(final Integer quantity) {
+ this.quantity = quantity;
}
- public void setCurrency(final Currency currency) {
- this.currency = currency;
+ public String getItemDetails() {
+ return itemDetails;
}
- public void setLinkedItemId(final UUID linkedItemId) {
- this.linkedItemId = linkedItemId;
+ public void setItemDetails(final String itemDetails) {
+ this.itemDetails = itemDetails;
}
@Override
@@ -242,6 +279,8 @@ public class InvoiceItemModelDao extends EntityModelDaoBase implements EntityMod
sb.append(", rate=").append(rate);
sb.append(", currency=").append(currency);
sb.append(", linkedItemId=").append(linkedItemId);
+ sb.append(", quantity=").append(quantity);
+ sb.append(", itemDetails=").append(itemDetails);
sb.append('}');
return sb.toString();
}
@@ -308,6 +347,12 @@ public class InvoiceItemModelDao extends EntityModelDaoBase implements EntityMod
if (type != that.type) {
return false;
}
+ if (quantity != null ? !quantity.equals(that.quantity) : that.quantity != null) {
+ return false;
+ }
+ if (itemDetails != null ? !itemDetails.equals(that.itemDetails) : that.itemDetails != null) {
+ return false;
+ }
return true;
}
@@ -331,6 +376,8 @@ public class InvoiceItemModelDao extends EntityModelDaoBase implements EntityMod
result = 31 * result + (rate != null ? rate.hashCode() : 0);
result = 31 * result + (currency != null ? currency.hashCode() : 0);
result = 31 * result + (linkedItemId != null ? linkedItemId.hashCode() : 0);
+ result = 31 * result + (quantity != null ? quantity.hashCode() : 0);
+ result = 31 * result + (itemDetails != null ? itemDetails.hashCode() : 0);
return result;
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java
index 64e6c4c..5fd4826 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java
@@ -45,6 +45,8 @@ import org.killbill.billing.invoice.usage.SubscriptionUsageInArrear;
import org.killbill.billing.invoice.usage.SubscriptionUsageInArrear.SubscriptionUsageInArrearItemsAndNextNotificationDate;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.junction.BillingEventSet;
+import org.killbill.billing.util.config.definition.InvoiceConfig;
+import org.killbill.billing.util.config.definition.InvoiceConfig.UsageDetailMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -61,10 +63,12 @@ public class UsageInvoiceItemGenerator extends InvoiceItemGenerator {
private static final Logger log = LoggerFactory.getLogger(UsageInvoiceItemGenerator.class);
private final RawUsageOptimizer rawUsageOptimizer;
+ private final InvoiceConfig invoiceConfig;
@Inject
- public UsageInvoiceItemGenerator(final RawUsageOptimizer rawUsageOptimizer) {
+ public UsageInvoiceItemGenerator(final RawUsageOptimizer rawUsageOptimizer, final InvoiceConfig invoiceConfig) {
this.rawUsageOptimizer = rawUsageOptimizer;
+ this.invoiceConfig = invoiceConfig;
}
@Override
@@ -80,7 +84,7 @@ public class UsageInvoiceItemGenerator extends InvoiceItemGenerator {
try {
// Pretty-print the generated invoice items from the junction events
final InvoiceItemGeneratorLogger invoiceItemGeneratorLogger = new InvoiceItemGeneratorLogger(invoiceId, account.getId(), "usage", log);
-
+ final UsageDetailMode usageDetailMode = invoiceConfig.getItemResultBehaviorMode(internalCallContext);
final LocalDate minBillingEventDate = getMinBillingEventDate(eventSet, internalCallContext);
final List<InvoiceItem> items = Lists.newArrayList();
@@ -115,7 +119,7 @@ public class UsageInvoiceItemGenerator extends InvoiceItemGenerator {
final UUID subscriptionId = event.getSubscription().getId();
if (curSubscriptionId != null && !curSubscriptionId.equals(subscriptionId)) {
- final SubscriptionUsageInArrear subscriptionUsageInArrear = new SubscriptionUsageInArrear(account.getId(), invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate(), internalCallContext);
+ final SubscriptionUsageInArrear subscriptionUsageInArrear = new SubscriptionUsageInArrear(account.getId(), invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate(), usageDetailMode, internalCallContext);
final List<InvoiceItem> usageInArrearItems = perSubscriptionInArrearUsageItems.get(curSubscriptionId);
final SubscriptionUsageInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionUsageInArrear.computeMissingUsageInvoiceItems(usageInArrearItems != null ? usageInArrearItems : ImmutableList.<InvoiceItem>of(), invoiceItemGeneratorLogger);
@@ -128,7 +132,7 @@ public class UsageInvoiceItemGenerator extends InvoiceItemGenerator {
curEvents.add(event);
}
if (curSubscriptionId != null) {
- final SubscriptionUsageInArrear subscriptionUsageInArrear = new SubscriptionUsageInArrear(account.getId(), invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate(), internalCallContext);
+ final SubscriptionUsageInArrear subscriptionUsageInArrear = new SubscriptionUsageInArrear(account.getId(), invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate(), usageDetailMode, internalCallContext);
final List<InvoiceItem> usageInArrearItems = perSubscriptionInArrearUsageItems.get(curSubscriptionId);
final SubscriptionUsageInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionUsageInArrear.computeMissingUsageInvoiceItems(usageInArrearItems != null ? usageInArrearItems : ImmutableList.<InvoiceItem>of(), invoiceItemGeneratorLogger);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
index 0e6ea00..b183ebe 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
@@ -53,23 +53,40 @@ public abstract class InvoiceItemBase extends EntityBase implements InvoiceItem
/* RepairAdjInvoiceItem */
protected final UUID linkedItemId;
+ /* Usage details */
+ protected final Integer quantity;
+ protected final String itemDetails;
public InvoiceItemBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId,
@Nullable final UUID subscriptionId, @Nullable final String description,
final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final BigDecimal rate, final Currency currency, final UUID reversedItemId) {
- this(id, createdDate, invoiceId, accountId, null, bundleId, subscriptionId, description, startDate, endDate, amount, rate, currency, reversedItemId);
+ this(id, createdDate, invoiceId, accountId, null, bundleId, subscriptionId, description, startDate, endDate, amount, rate, currency, reversedItemId, null, null);
+ }
+
+ public InvoiceItemBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId,
+ @Nullable final UUID subscriptionId, @Nullable final String description,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final BigDecimal rate, final Currency currency, final UUID reversedItemId,
+ @Nullable final Integer quantity, @Nullable final String itemDetails) {
+ this(id, createdDate, invoiceId, accountId, null, bundleId, subscriptionId, description, startDate, endDate, amount, rate, currency, reversedItemId, quantity, itemDetails);
}
// For parent invoices
public InvoiceItemBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, final UUID childAccountId,
- final BigDecimal amount, final Currency currency, final String description) {
- this(id, createdDate, invoiceId, accountId, childAccountId, null, null, description, null, null, amount, null, currency, null);
+ final BigDecimal amount, final Currency currency, final String description) {
+ this(id, createdDate, invoiceId, accountId, childAccountId, null, null, description, null, null, amount, null, currency, null, null, null);
+ }
+
+ public InvoiceItemBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID childAccountId, @Nullable final UUID bundleId,
+ @Nullable final UUID subscriptionId, @Nullable final String description,
+ @Nullable final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final BigDecimal rate, final Currency currency,
+ final UUID reversedItemId) {
+ this(id, createdDate, invoiceId, accountId, childAccountId, bundleId, subscriptionId, description, startDate, endDate, amount, rate, currency, reversedItemId, null, null);
}
private InvoiceItemBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID childAccountId, @Nullable final UUID bundleId,
@Nullable final UUID subscriptionId, @Nullable final String description,
@Nullable final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final BigDecimal rate, final Currency currency,
- final UUID reversedItemId) {
+ final UUID reversedItemId, @Nullable final Integer quantity, @Nullable final String itemDetails) {
super(id, createdDate, createdDate);
this.invoiceId = invoiceId;
this.accountId = accountId;
@@ -83,6 +100,8 @@ public abstract class InvoiceItemBase extends EntityBase implements InvoiceItem
this.currency = currency;
this.rate = rate;
this.linkedItemId = reversedItemId;
+ this.quantity = quantity;
+ this.itemDetails = itemDetails;
}
@Override
@@ -135,13 +154,11 @@ public abstract class InvoiceItemBase extends EntityBase implements InvoiceItem
return linkedItemId;
}
-
@Override
public UUID getChildAccountId() {
return childAccountId;
}
-
@Override
public String getPlanName() {
return null;
@@ -172,6 +189,15 @@ public abstract class InvoiceItemBase extends EntityBase implements InvoiceItem
return null;
}
+ @Override
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ @Override
+ public String getItemDetails() {
+ return itemDetails;
+ }
@Override
public boolean equals(final Object o) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemCatalogBase.java b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemCatalogBase.java
index 6ad9068..9ad25ce 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemCatalogBase.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemCatalogBase.java
@@ -40,15 +40,30 @@ public abstract class InvoiceItemCatalogBase extends InvoiceItemBase implements
public InvoiceItemCatalogBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId,
@Nullable final UUID subscriptionId, @Nullable final String description, @Nullable final String planName, @Nullable final String phaseName, @Nullable final String usageName,
final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final BigDecimal rate, final Currency currency, @Nullable final UUID linkedItemId) {
- this(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, description, planName, phaseName, usageName, null, null, null, startDate, endDate, amount, rate, currency, linkedItemId);
+ this(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, description, planName, phaseName, usageName, null, null, null, startDate, endDate, amount, rate, currency, linkedItemId, null, null);
}
+ public InvoiceItemCatalogBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId,
+ @Nullable final UUID subscriptionId, @Nullable final String description, @Nullable final String planName, @Nullable final String phaseName, @Nullable final String usageName,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final BigDecimal rate, final Currency currency, @Nullable final UUID linkedItemId,
+ @Nullable final Integer quantity, @Nullable final String itemDetails) {
+ this(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, description, planName, phaseName, usageName, null, null, null, startDate, endDate, amount, rate, currency, linkedItemId, quantity, itemDetails);
+ }
public InvoiceItemCatalogBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId,
@Nullable final UUID subscriptionId, @Nullable final String description, @Nullable final String planName, @Nullable final String phaseName, @Nullable final String usageName,
@Nullable final String prettyPlanName, @Nullable final String prettyPhaseName, @Nullable final String prettyUsageName,
final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final BigDecimal rate, final Currency currency, @Nullable final UUID linkedItemId) {
- super(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, description, startDate, endDate, amount, rate, currency, linkedItemId);
+ this(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, description, planName, phaseName, usageName, prettyPlanName, prettyPhaseName, prettyUsageName, startDate, endDate, amount, rate, currency, linkedItemId, null, null);
+ }
+
+
+ public InvoiceItemCatalogBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId,
+ @Nullable final UUID subscriptionId, @Nullable final String description, @Nullable final String planName, @Nullable final String phaseName, @Nullable final String usageName,
+ @Nullable final String prettyPlanName, @Nullable final String prettyPhaseName, @Nullable final String prettyUsageName,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final BigDecimal rate, final Currency currency, @Nullable final UUID linkedItemId,
+ @Nullable final Integer quantity, @Nullable final String itemDetails) {
+ super(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, description, startDate, endDate, amount, rate, currency, linkedItemId, quantity, itemDetails);
this.planName = planName;
this.phaseName = phaseName;
this.usageName = usageName;
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 e8eef20..a7d0924 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
@@ -69,6 +69,8 @@ public class InvoiceItemFactory {
final BigDecimal rate = invoiceItemModelDao.getRate();
final Currency currency = invoiceItemModelDao.getCurrency();
final UUID linkedItemId = invoiceItemModelDao.getLinkedItemId();
+ final Integer quantity = invoiceItemModelDao.getQuantity();
+ final String itemDetails = invoiceItemModelDao.getItemDetails();
final InvoiceItemType type = invoiceItemModelDao.getType();
@@ -101,7 +103,7 @@ public class InvoiceItemFactory {
item = new ItemAdjInvoiceItem(id, createdDate, invoiceId, accountId, startDate, description, amount, currency, linkedItemId);
break;
case USAGE:
- item = new UsageInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usageName, prettyPlanName, prettyPlanPhaseName, prettyUsageName, startDate, endDate, description, amount, currency);
+ item = new UsageInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usageName, prettyPlanName, prettyPlanPhaseName, prettyUsageName, startDate, endDate, description, amount, rate, currency, quantity, itemDetails);
break;
case TAX:
item = new TaxInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usageName, prettyPlanName, prettyPlanPhaseName, prettyUsageName, startDate, description, amount, currency, linkedItemId);
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 3f66f69..e5841aa 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
@@ -35,14 +35,21 @@ public class UsageInvoiceItem extends InvoiceItemCatalogBase {
public UsageInvoiceItem(final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId, @Nullable final UUID subscriptionId,
final String planName, final String phaseName, final String usageName,
final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency) {
- this(UUIDs.randomUUID(), null, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usageName, null, null, null, startDate, endDate, null, amount, currency);
+ this(UUIDs.randomUUID(), null, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usageName, null, null, null, startDate, endDate, null, amount, null, currency, null, null);
+ }
+
+ public UsageInvoiceItem(final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId, @Nullable final UUID subscriptionId,
+ final String planName, final String phaseName, final String usageName,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final BigDecimal rate, final Currency currency, @Nullable final Integer quantity, @Nullable final String itemDetails) {
+ this(UUIDs.randomUUID(), null, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usageName, null, null, null, startDate, endDate, null, amount, rate, currency, quantity, itemDetails);
}
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 String usageName,
final String prettyPlanName, final String prettyPhaseName, final String prettyUsageName,
- final LocalDate startDate, final LocalDate endDate, @Nullable final String description, final BigDecimal amount, final Currency currency) {
- super(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, description, planName, phaseName, usageName, prettyPlanName, prettyPhaseName, prettyUsageName, startDate, endDate, amount, null, currency, null);
+ final LocalDate startDate, final LocalDate endDate, @Nullable final String description, final BigDecimal amount, final BigDecimal rate,
+ final Currency currency, @Nullable final Integer quantity, @Nullable final String itemDetails) {
+ super(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, description, planName, phaseName, usageName, prettyPlanName, prettyPhaseName, prettyUsageName, startDate, endDate, amount, rate, currency, null, quantity, itemDetails);
}
@Override
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java
index f59f5d8..c411381 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java
@@ -193,6 +193,12 @@ public class DefaultInvoiceItemFormatter implements InvoiceItemFormatter {
}
@Override
+ public Integer getQuantity() { return item.getQuantity(); }
+
+ @Override
+ public String getItemDetails() { return item.getItemDetails(); }
+
+ @Override
public boolean matches(final Object other) {
throw new UnsupportedOperationException();
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java
index 26f5714..b49d684 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java
@@ -17,8 +17,10 @@
package org.killbill.billing.invoice.usage;
+import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -47,9 +49,15 @@ import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.usage.RawUsage;
import org.killbill.billing.usage.api.RolledUpUnit;
import org.killbill.billing.usage.api.RolledUpUsage;
+import org.killbill.billing.util.config.definition.InvoiceConfig.UsageDetailMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
@@ -85,6 +93,8 @@ public class ContiguousIntervalUsageInArrear {
private final AtomicBoolean isBuilt;
private final LocalDate rawUsageStartDate;
private final InternalTenantContext internalTenantContext;
+ private final UsageDetailMode usageDetailMode;
+ private static final ObjectMapper objectMapper = new ObjectMapper();
public ContiguousIntervalUsageInArrear(final Usage usage,
final UUID accountId,
@@ -92,6 +102,7 @@ public class ContiguousIntervalUsageInArrear {
final List<RawUsage> rawSubscriptionUsage,
final LocalDate targetDate,
final LocalDate rawUsageStartDate,
+ final UsageDetailMode usageDetailMode,
final InternalTenantContext internalTenantContext) {
this.usage = usage;
this.accountId = accountId;
@@ -104,6 +115,7 @@ public class ContiguousIntervalUsageInArrear {
this.billingEvents = Lists.newLinkedList();
this.transitionTimes = Lists.newLinkedList();
this.isBuilt = new AtomicBoolean(false);
+ this.usageDetailMode = usageDetailMode;
}
/**
@@ -184,9 +196,10 @@ public class ContiguousIntervalUsageInArrear {
final List<RolledUpUsage> allUsage = getRolledUpUsage();
for (final RolledUpUsage ru : allUsage) {
+ List<UsageInArrearDetail> toBeBilledUsageDetails = Lists.newLinkedList();
BigDecimal toBeBilledUsage = BigDecimal.ZERO;
if (usage.getUsageType() == UsageType.CAPACITY) {
- toBeBilledUsage = computeToBeBilledCapacityInArrear(ru.getRolledUpUnits());
+ toBeBilledUsageDetails.addAll(computeToBeBilledCapacityInArrear(ru.getRolledUpUnits()));
} else /* UsageType.CONSUMABLE */{
// Compute total price amount that should be billed for that period of time (and usage section) across unitTypes.
@@ -196,21 +209,33 @@ public class ContiguousIntervalUsageInArrear {
continue;
}
- final BigDecimal toBeBilledForUnit = computeToBeBilledConsumableInArrear(cur);
- toBeBilledUsage = toBeBilledUsage.add(toBeBilledForUnit);
+ toBeBilledUsageDetails.addAll(computeToBeBilledConsumableInArrear(cur));
}
+
}
+ toBeBilledUsage = toBeBilledForUnit(toBeBilledUsageDetails);
+
// Retrieves current price amount billed for that period of time (and usage section)
final Iterable<InvoiceItem> billedItems = getBilledItems(ru.getStart(), ru.getEnd(), existingUsage);
final BigDecimal billedUsage = computeBilledUsage(billedItems);
// Compare the two and add the missing piece if required.
if (!billedItems.iterator().hasNext() || billedUsage.compareTo(toBeBilledUsage) < 0) {
- final BigDecimal amountToBill = toBeBilledUsage.subtract(billedUsage);
+ toBeBilledUsageDetails = reconcileExistedBilledWithToBeBilled(billedItems, toBeBilledUsageDetails);
+ final BigDecimal amountToBill = toBeBilledForUnit(toBeBilledUsageDetails);
+
if (amountToBill.compareTo(BigDecimal.ZERO) > 0) {
- final InvoiceItem item = new UsageInvoiceItem(invoiceId, accountId, getBundleId(), getSubscriptionId(), getPlanName(),
- getPhaseName(), usage.getName(), ru.getStart(), ru.getEnd(), amountToBill, getCurrency());
- result.add(item);
+ if (UsageDetailMode.DETAIL == usageDetailMode && usage.getUsageType() == UsageType.CONSUMABLE){
+ for (UsageInArrearDetail toBeBilledUsageDetail : toBeBilledUsageDetails){
+ final InvoiceItem item = new UsageInvoiceItem(invoiceId, accountId, getBundleId(), getSubscriptionId(), getPlanName(),
+ getPhaseName(), usage.getName(), ru.getStart(), ru.getEnd(), toBeBilledUsageDetail.getAmount(), toBeBilledUsageDetail.getTierPrice(), getCurrency(),toBeBilledUsageDetail.getQuantity(),null);
+ result.add(item);
+ }
+ } else {
+ final InvoiceItem item = new UsageInvoiceItem(invoiceId, accountId, getBundleId(), getSubscriptionId(), getPlanName(),
+ getPhaseName(), usage.getName(), ru.getStart(), ru.getEnd(), amountToBill, null, getCurrency(),null, toJson(toBeBilledUsageDetails));
+ result.add(item);
+ }
}
}
}
@@ -361,12 +386,14 @@ public class ContiguousIntervalUsageInArrear {
* @throws CatalogApiException
*/
@VisibleForTesting
- BigDecimal computeToBeBilledCapacityInArrear(final List<RolledUpUnit> roUnits) throws CatalogApiException {
+ List<UsageInArrearDetail> computeToBeBilledCapacityInArrear(final List<RolledUpUnit> roUnits) throws CatalogApiException {
Preconditions.checkState(isBuilt.get());
final List<Tier> tiers = getCapacityInArrearTier(usage);
-
+ int tierNum = 0;
for (final Tier cur : tiers) {
+ tierNum++;
+ List<UsageInArrearDetail> toBeBilledDetails = Lists.newLinkedList();
boolean complies = true;
for (final RolledUpUnit ro : roUnits) {
final Limit tierLimit = getTierLimit(cur, ro.getUnitType());
@@ -376,9 +403,12 @@ public class ContiguousIntervalUsageInArrear {
complies = false;
break;
}
+ toBeBilledDetails.add(new UsageInArrearDetail(tierNum, ro.getUnitType(), cur.getRecurringPrice().getPrice(getCurrency()), ro.getAmount().intValue(), BigDecimal.ZERO, BigDecimal.ZERO, ""));
+
}
if (complies) {
- return cur.getRecurringPrice().getPrice(getCurrency());
+ toBeBilledDetails.get(toBeBilledDetails.size() - 1).setAmount(cur.getRecurringPrice().getPrice(getCurrency()));
+ return toBeBilledDetails;
}
}
// Probably invalid catalog config
@@ -394,7 +424,7 @@ public class ContiguousIntervalUsageInArrear {
* @throws CatalogApiException
*/
@VisibleForTesting
- BigDecimal computeToBeBilledConsumableInArrear(final RolledUpUnit roUnit) throws CatalogApiException {
+ List<UsageInArrearDetail> computeToBeBilledConsumableInArrear(final RolledUpUnit roUnit) throws CatalogApiException {
Preconditions.checkState(isBuilt.get());
final List<TieredBlock> tieredBlocks = getConsumableInArrearTieredBlocks(usage, roUnit.getUnitType());
@@ -403,19 +433,22 @@ public class ContiguousIntervalUsageInArrear {
case ALL_TIERS:
return computeToBeBilledConsumableInArrearWith_ALL_TIERS(tieredBlocks, roUnit.getAmount());
case TOP_TIER:
- return computeToBeBilledConsumableInArrearWith_TOP_TIER(tieredBlocks, roUnit.getAmount());
+ return Arrays.asList(computeToBeBilledConsumableInArrearWith_TOP_TIER(tieredBlocks, roUnit.getAmount()));
default:
throw new IllegalStateException("Unknown TierBlockPolicy " + usage.getTierBlockPolicy());
}
}
- BigDecimal computeToBeBilledConsumableInArrearWith_ALL_TIERS(final List<TieredBlock> tieredBlocks, final Long units) throws CatalogApiException {
+ List<UsageInArrearDetail> computeToBeBilledConsumableInArrearWith_ALL_TIERS(final List<TieredBlock> tieredBlocks, final Long units) throws CatalogApiException {
+ List<UsageInArrearDetail> toBeBilledDetails = Lists.newLinkedList();
BigDecimal result = BigDecimal.ZERO;
int remainingUnits = units.intValue();
+ int tierNum = 0;
for (final TieredBlock tieredBlock : tieredBlocks) {
+ tierNum++;
final int blockTierSize = tieredBlock.getSize().intValue();
final int tmp = remainingUnits / blockTierSize + (remainingUnits % blockTierSize == 0 ? 0 : 1);
final int nbUsedTierBlocks;
@@ -426,32 +459,40 @@ public class ContiguousIntervalUsageInArrear {
nbUsedTierBlocks = tmp;
remainingUnits = 0;
}
- result = result.add(tieredBlock.getPrice().getPrice(getCurrency()).multiply(new BigDecimal(nbUsedTierBlocks)));
+
+ if (nbUsedTierBlocks > 0) {
+ toBeBilledDetails.add(new UsageInArrearDetail(tierNum, tieredBlock.getUnit().getName(), tieredBlock.getPrice().getPrice(getCurrency()), nbUsedTierBlocks));
+ }
}
- return result;
+ return toBeBilledDetails;
}
- BigDecimal computeToBeBilledConsumableInArrearWith_TOP_TIER(final List<TieredBlock> tieredBlocks, final Long units) throws CatalogApiException {
+ UsageInArrearDetail computeToBeBilledConsumableInArrearWith_TOP_TIER(final List<TieredBlock> tieredBlocks, final Long units) throws CatalogApiException {
int remainingUnits = units.intValue();
// By default last last tierBlock
TieredBlock targetBlock = tieredBlocks.get(tieredBlocks.size() - 1);
+ int targetTierNum = tieredBlocks.size();
+ int tierNum = 0;
// Loop through all tier block
for (final TieredBlock tieredBlock : tieredBlocks) {
+ tierNum++;
final int blockTierSize = tieredBlock.getSize().intValue();
final int tmp = remainingUnits / blockTierSize + (remainingUnits % blockTierSize == 0 ? 0 : 1);
if (tmp > tieredBlock.getMax()) {
remainingUnits -= tieredBlock.getMax() * blockTierSize;
} else {
targetBlock = tieredBlock;
+ targetTierNum = tierNum;
break;
}
}
final int lastBlockTierSize = targetBlock.getSize().intValue();
final int nbBlocks = units.intValue() / lastBlockTierSize + (units.intValue() % lastBlockTierSize == 0 ? 0 : 1);
- return targetBlock.getPrice().getPrice(getCurrency()).multiply(new BigDecimal(nbBlocks));
+
+ return new UsageInArrearDetail(targetTierNum, targetBlock.getUnit().getName(), targetBlock.getPrice().getPrice(getCurrency()), nbBlocks);
}
@@ -557,4 +598,156 @@ public class ContiguousIntervalUsageInArrear {
return nextNotificationDate;
}
}
+
+ public BigDecimal toBeBilledForUnit(List<UsageInArrearDetail> toBeBilledDetails){
+ BigDecimal result = BigDecimal.ZERO;
+ for (UsageInArrearDetail toBeBilled: toBeBilledDetails){
+ result = result.add(toBeBilled.getAmount());
+ }
+ return result;
+ }
+
+ private List<UsageInArrearDetail> reconcileExistedBilledWithToBeBilled(Iterable<InvoiceItem> billedItems, List<UsageInArrearDetail> toBeBilledUsageInArrearDetails) {
+ for (final InvoiceItem bi : billedItems) {
+ List<UsageInArrearDetail> billedUsageItemDetails = fromJson(bi.getItemDetails());
+
+ if (billedUsageItemDetails != null && billedUsageItemDetails.size() > 0) {
+
+ for (final UsageInArrearDetail toBeBilledConsumable : toBeBilledUsageInArrearDetails) {
+ billedUsageItemDetails = toBeBilledConsumable.reconcile(billedUsageItemDetails);
+ }
+
+ if (billedUsageItemDetails != null && billedUsageItemDetails.size() > 0) {
+ for (final UsageInArrearDetail billedUsage : billedUsageItemDetails) {
+ toBeBilledUsageInArrearDetails.add(new UsageInArrearDetail(billedUsage.getTier(), billedUsage.getTierUnit(), billedUsage.getTierPrice(),
+ billedUsage.getQuantity() * -1, billedUsage.getAmount().negate(), null, bi.getId().toString()));
+ }
+ }
+ } else {
+ toBeBilledUsageInArrearDetails.get(0).setAmount(toBeBilledUsageInArrearDetails.get(0).getAmount().subtract(bi.getAmount()));
+ toBeBilledUsageInArrearDetails.get(0).setQuantity(toBeBilledUsageInArrearDetails.get(0).getQuantity() - (bi.getQuantity() == null ? 0 : bi.getQuantity()));
+ toBeBilledUsageInArrearDetails.get(0).setExistingUsageAmount(toBeBilledUsageInArrearDetails.get(0).getExistingUsageAmount().add(bi.getAmount()));
+ toBeBilledUsageInArrearDetails.get(0).setReference(toBeBilledUsageInArrearDetails.get(0).getReference().concat(":").concat(bi.getId().toString()));
+ }
+ }
+
+ return toBeBilledUsageInArrearDetails;
+ }
+
+ private static final String toJson(List<UsageInArrearDetail> toBeBilledUsageInArrearDetails) {
+ String result = null;
+ if (toBeBilledUsageInArrearDetails != null && toBeBilledUsageInArrearDetails.size() > 0){
+ try {
+ result = objectMapper.writeValueAsString(toBeBilledUsageInArrearDetails);
+ } catch (JsonProcessingException e) {
+ Preconditions.checkState(false, e.getMessage());
+ }
+ }
+ return result;
+ }
+
+ private static final List<UsageInArrearDetail> fromJson(String itemDetails){
+ List<UsageInArrearDetail> toBeBilledUsageInArrearDetails = null;
+ if (itemDetails != null){
+ try {
+ toBeBilledUsageInArrearDetails = objectMapper.readValue(itemDetails, new TypeReference<List<UsageInArrearDetail>>() {});
+ } catch (IOException e) {
+ Preconditions.checkState(false, e.getMessage());
+ }
+ }
+
+ return toBeBilledUsageInArrearDetails;
+ }
+
+ public static class UsageInArrearDetail {
+
+ private final int tier;
+ private final String tierUnit;
+ private final BigDecimal tierPrice;
+ private Integer quantity;
+ private String reference;
+ private BigDecimal existingUsageAmount;
+ private BigDecimal amount;
+
+ public UsageInArrearDetail(BigDecimal tierPrice, Integer quantity, BigDecimal existingUsageAmount, String reference) {
+ this(0, null, tierPrice, quantity, existingUsageAmount, BigDecimal.ZERO, reference);
+ }
+
+ public UsageInArrearDetail(int tier, String tierUnit, BigDecimal tierPrice, Integer quantity) {
+ this(tier, tierUnit, tierPrice, quantity, tierPrice.multiply(new BigDecimal(quantity)), BigDecimal.ZERO, "");
+ }
+
+ @JsonCreator
+ public UsageInArrearDetail(@JsonProperty("tier") int tier, @JsonProperty("tierUnit") String tierUnit,
+ @JsonProperty("tierPrice") BigDecimal tierPrice, @JsonProperty("quantity") Integer quantity,
+ @JsonProperty("amount") BigDecimal amount, @JsonProperty("existingUsageAmount") BigDecimal existingUsageAmount,
+ @JsonProperty("reference") String reference) {
+ this.tier = tier;
+ this.tierUnit = tierUnit;
+ this.tierPrice = tierPrice;
+ this.quantity = quantity;
+ this.amount = amount;
+ this.existingUsageAmount = existingUsageAmount;
+ this.reference = reference;
+ }
+
+ public int getTier() {
+ return tier;
+ }
+
+ public String getTierUnit() {
+ return tierUnit;
+ }
+
+ public BigDecimal getTierPrice() {
+ return tierPrice;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public BigDecimal getExistingUsageAmount() {
+ return existingUsageAmount;
+ }
+
+ public String getReference() {
+ return reference;
+ }
+
+ public void setExistingUsageAmount(BigDecimal existingUsageAmount) {
+ this.existingUsageAmount = existingUsageAmount;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+
+ public void setAmount(BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ public void setReference(String reference) {
+ this.reference = reference;
+ }
+
+ public List<UsageInArrearDetail> reconcile(List<UsageInArrearDetail> billedUsageItemDetails) {
+ List<UsageInArrearDetail> unreconciledUsage = Lists.newLinkedList();
+ for (UsageInArrearDetail billedUsageDetail : billedUsageItemDetails) {
+ if (tierUnit.equals(billedUsageDetail.tierUnit)) {
+ existingUsageAmount = billedUsageDetail.getAmount().abs();
+ quantity = quantity - billedUsageDetail.getQuantity();
+ amount = amount.subtract(existingUsageAmount);
+ } else {
+ unreconciledUsage.add(billedUsageDetail);
+ }
+ }
+
+ return unreconciledUsage;
+ }
+ }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java
index 726706c..b104127 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java
@@ -38,6 +38,8 @@ import org.killbill.billing.invoice.generator.InvoiceItemGenerator.InvoiceItemGe
import org.killbill.billing.invoice.usage.ContiguousIntervalUsageInArrear.UsageInArrearItemsAndNextNotificationDate;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.usage.RawUsage;
+import org.killbill.billing.util.config.definition.InvoiceConfig;
+import org.killbill.billing.util.config.definition.InvoiceConfig.UsageDetailMode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
@@ -78,6 +80,7 @@ public class SubscriptionUsageInArrear {
private final List<RawUsage> rawSubscriptionUsage;
private final LocalDate rawUsageStartDate;
private final InternalTenantContext internalTenantContext;
+ private final UsageDetailMode usageDetailMode;
public SubscriptionUsageInArrear(final UUID accountId,
final UUID invoiceId,
@@ -85,6 +88,7 @@ public class SubscriptionUsageInArrear {
final List<RawUsage> rawUsage,
final LocalDate targetDate,
final LocalDate rawUsageStartDate,
+ final UsageDetailMode usageDetailMode,
final InternalTenantContext internalTenantContext) {
this.accountId = accountId;
@@ -100,6 +104,7 @@ public class SubscriptionUsageInArrear {
return input.getSubscriptionId().equals(subscriptionBillingEvents.get(0).getSubscription().getId());
}
}));
+ this.usageDetailMode = usageDetailMode;
}
/**
@@ -112,12 +117,12 @@ public class SubscriptionUsageInArrear {
final SubscriptionUsageInArrearItemsAndNextNotificationDate result = new SubscriptionUsageInArrearItemsAndNextNotificationDate();
final List<ContiguousIntervalUsageInArrear> billingEventTransitionTimePeriods = computeInArrearUsageInterval();
for (final ContiguousIntervalUsageInArrear usageInterval : billingEventTransitionTimePeriods) {
- final UsageInArrearItemsAndNextNotificationDate newItemsAndDate = usageInterval.computeMissingItemsAndNextNotificationDate(existingUsage);
+ final UsageInArrearItemsAndNextNotificationDate newItemsWithDetailsAndDate = usageInterval.computeMissingItemsAndNextNotificationDate(existingUsage);
// For debugging purposes
- invoiceItemGeneratorLogger.append(usageInterval, newItemsAndDate.getInvoiceItems());
+ invoiceItemGeneratorLogger.append(usageInterval, newItemsWithDetailsAndDate.getInvoiceItems());
- result.addUsageInArrearItemsAndNextNotificationDate(usageInterval.getUsage().getName(), newItemsAndDate);
+ result.addUsageInArrearItemsAndNextNotificationDate(usageInterval.getUsage().getName(), newItemsWithDetailsAndDate);
}
return result;
}
@@ -149,7 +154,7 @@ public class SubscriptionUsageInArrear {
// Add inflight usage interval if non existent
ContiguousIntervalUsageInArrear existingInterval = inFlightInArrearUsageIntervals.get(usage.getName());
if (existingInterval == null) {
- existingInterval = new ContiguousIntervalUsageInArrear(usage, accountId, invoiceId, rawSubscriptionUsage, targetDate, rawUsageStartDate, internalTenantContext);
+ existingInterval = new ContiguousIntervalUsageInArrear(usage, accountId, invoiceId, rawSubscriptionUsage, targetDate, rawUsageStartDate, usageDetailMode, internalTenantContext);
inFlightInArrearUsageIntervals.put(usage.getName(), existingInterval);
}
// Add billing event for that usage interval
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/UsageUtils.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/UsageUtils.java
index 0a39859..69baa60 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/UsageUtils.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/UsageUtils.java
@@ -16,8 +16,10 @@
package org.killbill.billing.invoice.usage;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import org.killbill.billing.catalog.api.BillingMode;
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg
index c7596b4..eccd1c5 100644
--- a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg
@@ -19,6 +19,8 @@ tableFields(prefix) ::= <<
, <prefix>rate
, <prefix>currency
, <prefix>linked_item_id
+, <prefix>quantity
+, <prefix>item_details
, <prefix>created_by
, <prefix>created_date
>>
@@ -40,6 +42,8 @@ tableValues() ::= <<
, :rate
, :currency
, :linkedItemId
+, :quantity
+, :itemDetails
, :createdBy
, :createdDate
>>
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql b/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql
index 52f4ee7..f2bce36 100644
--- a/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql
@@ -20,6 +20,8 @@ CREATE TABLE invoice_items (
rate numeric(15,9) NULL,
currency varchar(3) NOT NULL,
linked_item_id varchar(36),
+ quantity int,
+ item_details text,
created_by varchar(50) NOT NULL,
created_date datetime NOT NULL,
account_record_id bigint /*! unsigned */ not null,
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20180123114605__invoice_item_quantity_item_details.sql b/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20180123114605__invoice_item_quantity_item_details.sql
new file mode 100644
index 0000000..a78535d
--- /dev/null
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20180123114605__invoice_item_quantity_item_details.sql
@@ -0,0 +1,2 @@
+alter table invoice_items add column quantity int after linked_item_id;
+alter table invoice_items add column item_details text after quantity;
\ No newline at end of file
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
index dcd4a44..dff38c1 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
@@ -39,6 +39,8 @@ import org.killbill.billing.usage.api.UsageUserApi;
import org.killbill.billing.util.api.TagUserApi;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.config.definition.InvoiceConfig;
+import org.killbill.billing.util.config.definition.InvoiceConfig.UsageDetailMode;
import org.killbill.bus.api.PersistentBus;
import org.killbill.clock.Clock;
import org.killbill.commons.locker.GlobalLocker;
@@ -67,6 +69,8 @@ public abstract class InvoiceTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
@Inject
protected InvoiceGenerator generator;
@Inject
+ protected InvoiceConfig invoiceConfig;
+ @Inject
protected BillingInternalApi billingApi;
@Inject
protected AccountInternalApi accountApi;
@@ -104,6 +108,7 @@ public abstract class InvoiceTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
protected KillbillConfigSource getConfigSource() {
return getConfigSource("/resource.properties");
}
+ protected UsageDetailMode usageDetailMode;
@BeforeClass(groups = "fast")
protected void beforeClass() throws Exception {
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalCapacityInArrear.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalCapacityInArrear.java
index f64b95e..14cbf50 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalCapacityInArrear.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalCapacityInArrear.java
@@ -17,6 +17,7 @@
package org.killbill.billing.invoice.usage;
+import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
@@ -32,27 +33,27 @@ import org.killbill.billing.catalog.DefaultUsage;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.TierBlockPolicy;
import org.killbill.billing.catalog.api.Usage;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
import org.killbill.billing.invoice.model.UsageInvoiceItem;
+import org.killbill.billing.invoice.usage.ContiguousIntervalUsageInArrear.UsageInArrearDetail;
import org.killbill.billing.invoice.usage.ContiguousIntervalUsageInArrear.UsageInArrearItemsAndNextNotificationDate;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.usage.RawUsage;
import org.killbill.billing.usage.api.RolledUpUnit;
-import org.killbill.billing.usage.api.RolledUpUsage;
import org.killbill.billing.usage.api.svcs.DefaultRawUsage;
-import org.testng.Assert;
+import org.killbill.billing.util.config.definition.InvoiceConfig.UsageDetailMode;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
-import com.google.common.base.Function;
+import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
-import com.google.common.collect.Ordering;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
@@ -119,19 +120,23 @@ public class TestContiguousIntervalCapacityInArrear extends TestUsageInArrearBas
final DefaultUnit unit1 = new DefaultUnit().setName("unit1");
final DefaultUnit unit2 = new DefaultUnit().setName("unit2");
+ final DefaultUnit unit3 = new DefaultUnit().setName("unit3");
final DefaultLimit limit1_1 = new DefaultLimit().setUnit(unit1).setMax((double) 100).setMin((double) -1);
final DefaultLimit limit1_2 = new DefaultLimit().setUnit(unit2).setMax((double) 1000).setMin((double) -1);
- final DefaultTier tier1 = createDefaultTierWithLimits(BigDecimal.TEN, limit1_1, limit1_2);
+ final DefaultLimit limit1_3 = new DefaultLimit().setUnit(unit3).setMax((double) 50).setMin((double) -1);
+ final DefaultTier tier1 = createDefaultTierWithLimits(BigDecimal.TEN, limit1_1, limit1_2, limit1_3);
final DefaultLimit limit2_1 = new DefaultLimit().setUnit(unit1).setMax((double) 200).setMin((double) -1);
final DefaultLimit limit2_2 = new DefaultLimit().setUnit(unit2).setMax((double) 2000).setMin((double) -1);
- final DefaultTier tier2 = createDefaultTierWithLimits(new BigDecimal("20.0"), limit2_1, limit2_2);
+ final DefaultLimit limit2_3 = new DefaultLimit().setUnit(unit3).setMax((double) 100).setMin((double) -1);
+ final DefaultTier tier2 = createDefaultTierWithLimits(new BigDecimal("20.0"), limit2_1, limit2_2, limit2_3);
// Don't define any max for last tier to allow any number
final DefaultLimit limit3_1 = new DefaultLimit().setUnit(unit1).setMin((double) -1).setMax((double) -1);
final DefaultLimit limit3_2 = new DefaultLimit().setUnit(unit2).setMin((double) -1).setMax((double) -1);
- final DefaultTier tier3 = createDefaultTierWithLimits(new BigDecimal("30.0"), limit3_1, limit3_2);
+ final DefaultLimit limit3_3 = new DefaultLimit().setUnit(unit3).setMax((double) -1).setMin((double) -1);
+ final DefaultTier tier3 = createDefaultTierWithLimits(new BigDecimal("30.0"), limit3_1, limit3_2, limit3_3);
final DefaultUsage usage = createCapacityInArrearUsage(usageName, BillingPeriod.MONTHLY, tier1, tier2, tier3);
@@ -144,30 +149,32 @@ public class TestContiguousIntervalCapacityInArrear extends TestUsageInArrearBas
Collections.<Usage>emptyList())
);
// Tier 1 (both units from tier 1)
- BigDecimal result = intervalCapacityInArrear.computeToBeBilledCapacityInArrear(ImmutableList.<RolledUpUnit>of(new DefaultRolledUpUnit("unit1", 100L),
- new DefaultRolledUpUnit("unit2", 1000L)));
- assertEquals(result, BigDecimal.TEN);
+ List<UsageInArrearDetail> result = intervalCapacityInArrear.computeToBeBilledCapacityInArrear(ImmutableList.<RolledUpUnit>of(new DefaultRolledUpUnit("unit1", 100L),
+ new DefaultRolledUpUnit("unit2", 1000L),
+ new DefaultRolledUpUnit("unit3", 50L)));
+ assertEquals(result.size(), 3);
+ assertTrue(intervalCapacityInArrear.toBeBilledForUnit(result).compareTo(BigDecimal.TEN) == 0);
// Tier 2 (only one unit from tier 1)
result = intervalCapacityInArrear.computeToBeBilledCapacityInArrear(ImmutableList.<RolledUpUnit>of(new DefaultRolledUpUnit("unit1", 100L),
new DefaultRolledUpUnit("unit2", 1001L)));
- assertEquals(result, new BigDecimal("20.0"));
+ assertTrue(intervalCapacityInArrear.toBeBilledForUnit(result).compareTo(new BigDecimal("20.0")) == 0);
// Tier 2 (only one unit from tier 1)
result = intervalCapacityInArrear.computeToBeBilledCapacityInArrear(ImmutableList.<RolledUpUnit>of(new DefaultRolledUpUnit("unit1", 101L),
new DefaultRolledUpUnit("unit2", 1000L)));
- assertEquals(result, new BigDecimal("20.0"));
+ assertTrue(intervalCapacityInArrear.toBeBilledForUnit(result).compareTo(new BigDecimal("20.0")) == 0);
// Tier 2 (both units from tier 2)
result = intervalCapacityInArrear.computeToBeBilledCapacityInArrear(ImmutableList.<RolledUpUnit>of(new DefaultRolledUpUnit("unit1", 101L),
new DefaultRolledUpUnit("unit2", 1001L)));
- assertEquals(result, new BigDecimal("20.0"));
+ assertTrue(intervalCapacityInArrear.toBeBilledForUnit(result).compareTo(new BigDecimal("20.0")) == 0);
// Tier 3 (only one unit from tier 3)
result = intervalCapacityInArrear.computeToBeBilledCapacityInArrear(ImmutableList.<RolledUpUnit>of(new DefaultRolledUpUnit("unit1", 10L),
new DefaultRolledUpUnit("unit2", 2001L)));
- assertEquals(result, new BigDecimal("30.0"));
+ assertTrue(intervalCapacityInArrear.toBeBilledForUnit(result).compareTo(new BigDecimal("30.0")) == 0);
}
@Test(groups = "fast")
@@ -241,6 +248,135 @@ public class TestContiguousIntervalCapacityInArrear extends TestUsageInArrearBas
assertTrue(result.get(1).getEndDate().compareTo(endDate) == 0);
}
+ @Test(groups = "fast")
+ public void testComputeMissingItemsAggregateMode() throws CatalogApiException, IOException {
+
+ testComputeMissingItemsByMode(UsageDetailMode.AGGREGATE);
+ }
+
+ @Test(groups = "fast")
+ public void testComputeMissingItemsDetailMode() throws CatalogApiException, IOException {
+
+ testComputeMissingItemsByMode(UsageDetailMode.DETAIL);
+ }
+
+ private void testComputeMissingItemsByMode(UsageDetailMode usageDetailMode) throws CatalogApiException, IOException {
+
+ // Case 1
+ List<RawUsage> rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 99L));
+
+ List<InvoiceItem> result = produceInvoiceItems(rawUsages, usageDetailMode);
+ assertEquals(result.size(), 1);
+ assertEquals(result.get(0).getAmount().compareTo(BigDecimal.ONE),0, String.format("%s != 1.0", result.get(0).getAmount()));
+
+ List<UsageInArrearDetail> itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ // BAR item detail
+ assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(0).getTier(), 1);
+ assertEquals(itemDetails.get(0).getQuantity().intValue(), 99);
+ assertEquals(itemDetails.get(0).getTierPrice().compareTo(BigDecimal.ONE), 0);
+ // FOO item detail
+ assertEquals(itemDetails.get(1).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(1).getTier(), 1);
+ assertEquals(itemDetails.get(1).getQuantity().intValue(), 5);
+ assertEquals(itemDetails.get(1).getTierPrice().compareTo(BigDecimal.ONE), 0);
+
+ // Case 2
+ rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
+ result = produceInvoiceItems(rawUsages, usageDetailMode);
+ assertEquals(result.size(), 1);
+ assertEquals(result.get(0).getAmount().compareTo(BigDecimal.TEN),0, String.format("%s != 10.0", result.get(0).getAmount()));
+
+ itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ // BAR item detail
+ assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(0).getTier(), 2);
+ assertEquals(itemDetails.get(0).getQuantity().intValue(), 101);
+ assertEquals(itemDetails.get(0).getTierPrice().compareTo(BigDecimal.TEN), 0);
+ // FOO item detail
+ assertEquals(itemDetails.get(1).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(1).getTier(), 2);
+ assertEquals(itemDetails.get(1).getQuantity().intValue(), 5);
+ assertEquals(itemDetails.get(1).getTierPrice().compareTo(BigDecimal.TEN), 0);
+
+ // Case 3
+ rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 75L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
+ result = produceInvoiceItems(rawUsages, usageDetailMode);
+ assertEquals(result.size(), 1);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("100.0")),0, String.format("%s != 100.0", result.get(0).getAmount()));
+
+ itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ // BAR item detail
+ assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(0).getTier(), 3);
+ assertEquals(itemDetails.get(0).getQuantity().intValue(), 101);
+ assertEquals(itemDetails.get(0).getTierPrice().compareTo(new BigDecimal("100.0")), 0);
+ // FOO item detail
+ assertEquals(itemDetails.get(1).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(1).getTier(), 3);
+ assertEquals(itemDetails.get(1).getQuantity().intValue(), 75);
+ assertEquals(itemDetails.get(1).getTierPrice().compareTo(new BigDecimal("100.0")), 0);
+ }
+
+ private List<InvoiceItem> produceInvoiceItems(List<RawUsage> rawUsages, UsageDetailMode usageDetailMode) throws CatalogApiException {
+
+ final LocalDate startDate = new LocalDate(2014, 03, 20);
+ final LocalDate firstBCDDate = new LocalDate(2014, 04, 15);
+ final LocalDate endDate = new LocalDate(2014, 05, 15);
+
+ final DefaultUnit unitFoo = new DefaultUnit().setName("FOO");
+ final DefaultUnit unitBar = new DefaultUnit().setName("BAR");
+
+ final DefaultLimit unitFooLimitTier1 = new DefaultLimit().setUnit(unitFoo).setMax((double) 10);
+ final DefaultLimit unitBarLimitTier1 = new DefaultLimit().setUnit(unitBar).setMax((double) 100);
+ final DefaultTier tier1 = createDefaultTierWithLimits(BigDecimal.ONE, unitFooLimitTier1, unitBarLimitTier1);
+
+ final DefaultLimit unitFooLimitTier2 = new DefaultLimit().setUnit(unitFoo).setMax((double) 50);
+ final DefaultLimit unitBarLimitTier2 = new DefaultLimit().setUnit(unitBar).setMax((double) 500);
+ final DefaultTier tier2 = createDefaultTierWithLimits(BigDecimal.TEN, unitFooLimitTier2, unitBarLimitTier2);
+
+ final DefaultLimit unitFooLimitTier3 = new DefaultLimit().setUnit(unitFoo).setMax((double) 75);
+ final DefaultLimit unitBarLimitTier3 = new DefaultLimit().setUnit(unitBar).setMax((double) 750);
+ final DefaultTier tier3 = createDefaultTierWithLimits(new BigDecimal("100.0"), unitFooLimitTier3, unitBarLimitTier3);
+
+ final DefaultUsage usage = createCapacityInArrearUsage(usageName, BillingPeriod.MONTHLY, tier1, tier2, tier3);
+
+ final LocalDate targetDate = endDate;
+
+ final BillingEvent event1 = createMockBillingEvent(startDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),BillingPeriod.MONTHLY, Collections.<Usage>emptyList());
+ final BillingEvent event2 = createMockBillingEvent(endDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.<Usage>emptyList());
+
+ final ContiguousIntervalUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, rawUsages, targetDate, true, usageDetailMode, event1, event2);
+
+ final UsageInArrearItemsAndNextNotificationDate usageResult = intervalConsumableInArrear.computeMissingItemsAndNextNotificationDate(ImmutableList.<InvoiceItem>of());
+ final List<InvoiceItem> rawResults = usageResult.getInvoiceItems();
+ final List<InvoiceItem> result = ImmutableList.copyOf(Iterables.filter(rawResults, new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ return input.getAmount().compareTo(BigDecimal.ZERO) > 0;
+ }
+ }));
+
+ for (InvoiceItem item: result) {
+ assertEquals(item.getCurrency(), Currency.BTC);
+ assertEquals(item.getAccountId(), accountId);
+ assertEquals(item.getBundleId(), bundleId);
+ assertEquals(item.getSubscriptionId(), subscriptionId);
+ assertEquals(item.getPlanName(), planName);
+ assertEquals(item.getPhaseName(), phaseName);
+ assertEquals(item.getUsageName(), usage.getName());
+ assertTrue(item.getStartDate().compareTo(startDate) == 0);
+ assertTrue(item.getEndDate().compareTo(firstBCDDate) == 0);
+ }
+
+ return result;
+ }
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java
index 6ad767e..3c63a05 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java
@@ -18,10 +18,14 @@
package org.killbill.billing.invoice.usage;
+import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
@@ -36,16 +40,22 @@ import org.killbill.billing.catalog.api.Usage;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
import org.killbill.billing.invoice.model.UsageInvoiceItem;
+import org.killbill.billing.invoice.usage.ContiguousIntervalUsageInArrear.UsageInArrearDetail;
import org.killbill.billing.invoice.usage.ContiguousIntervalUsageInArrear.UsageInArrearItemsAndNextNotificationDate;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.usage.RawUsage;
import org.killbill.billing.usage.api.RolledUpUsage;
import org.killbill.billing.usage.api.svcs.DefaultRawUsage;
+import org.killbill.billing.util.config.definition.InvoiceConfig.UsageDetailMode;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
@@ -139,10 +149,12 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
Collections.<Usage>emptyList())
);
- final BigDecimal result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 111L));
-
+ List<UsageInArrearDetail> result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 111L));
+ assertEquals(result.size(), 3);
// 111 = 10 (tier1) + 100 (tier2) + 1 (tier3) => 10 * 1.5 + 100 * 1 + 1 * 0.5 = 115.5
- assertEquals(result, new BigDecimal("115.5"));
+ assertEquals(result.get(0).getAmount(), new BigDecimal("15.0"));
+ assertEquals(result.get(1).getAmount(), new BigDecimal("100.0"));
+ assertEquals(result.get(2).getAmount(), new BigDecimal("0.5"));
}
@@ -165,10 +177,12 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
Collections.<Usage>emptyList())
);
- final BigDecimal result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 5325L));
+ List<UsageInArrearDetail> result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 5325L));
+ assertEquals(result.size(), 2);
// 5000 = 1000 (tier1) + 4325 (tier2) => 10 + 5 = 15
- assertEquals(result, new BigDecimal("15"));
+ assertEquals(result.get(0).getAmount(), new BigDecimal("10"));
+ assertEquals(result.get(1).getAmount(), new BigDecimal("5"));
}
@@ -197,22 +211,26 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
//
// In this model unit amount is first used to figure out which tier we are in, and then we price all unit at that 'target' tier
//
- final BigDecimal inputTier1 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 1000L));
+ List<UsageInArrearDetail> inputTier1 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 1000L));
+ assertEquals(inputTier1.size(), 1);
// 1000 units => (tier1) : 1000 / 100 + 1000 % 100 = 10
- assertEquals(inputTier1, new BigDecimal("10"));
+ assertEquals(inputTier1.get(0).getAmount(), new BigDecimal("10"));
- final BigDecimal inputTier2 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 101000L));
+ List<UsageInArrearDetail> inputTier2 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 101000L));
+ assertEquals(inputTier2.size(), 1);
// 101000 units => (tier2) : 101000 / 1000 + 101000 % 1000 = 101 + 0 = 101
- assertEquals(inputTier2, new BigDecimal("101"));
+ assertEquals(inputTier2.get(0).getAmount(), new BigDecimal("101"));
- final BigDecimal inputTier3 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 101001L));
+ List<UsageInArrearDetail> inputTier3 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 101001L));
+ assertEquals(inputTier3.size(), 1);
// 101001 units => (tier3) : 101001 / 1000 + 101001 % 1000 = 101 + 1 = 102 units => $51
- assertEquals(inputTier3, new BigDecimal("51.0"));
+ assertEquals(inputTier3.get(0).getAmount(), new BigDecimal("51.0"));
// If we pass the maximum of the last tier, we price all units at the last tier
- final BigDecimal inputLastTier = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 300000L));
+ List<UsageInArrearDetail> inputLastTier = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 300000L));
+ assertEquals(inputLastTier.size(), 1);
// 300000 units => (tier3) : 300000 / 1000 + 300000 % 1000 = 300 units => $150
- assertEquals(inputLastTier, new BigDecimal("150.0"));
+ assertEquals(inputLastTier.get(0).getAmount(), new BigDecimal("150.0"));
}
@@ -238,17 +256,18 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
Collections.<Usage>emptyList())
);
- final BigDecimal result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 111L));
+ List<UsageInArrearDetail> result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 111L));
+ assertEquals(result.size(), 1);
// 111 = 111 * 0.5 =
- assertEquals(result, new BigDecimal("55.5"));
+ assertEquals(result.get(0).getAmount(), new BigDecimal("55.5"));
}
@Test(groups = "fast")
- public void testComputeMissingItems() throws CatalogApiException {
+ public void testComputeMissingItems() throws CatalogApiException, IOException {
final LocalDate startDate = new LocalDate(2014, 03, 20);
final LocalDate firstBCDDate = new LocalDate(2014, 04, 15);
@@ -290,7 +309,6 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
}
}));
-
// Invoiced for 1 BTC and used 130 + 271 = 401 => 5 blocks => 5 BTC so remaining piece should be 4 BTC
assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("4.0")), 0, String.format("%s != 4.0", result.get(0).getAmount()));
assertEquals(result.get(0).getCurrency(), Currency.BTC);
@@ -303,6 +321,12 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
assertTrue(result.get(0).getStartDate().compareTo(startDate) == 0);
assertTrue(result.get(0).getEndDate().compareTo(firstBCDDate) == 0);
+ // check item detail
+ List<UsageInArrearDetail> itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+
+ assertEquals(itemDetails.size(), 1);
+ assertEquals(itemDetails.get(0).getAmount().compareTo(result.get(0).getAmount()),0);
+
// Invoiced for 1 BTC and used 199 => 2 blocks => 2 BTC so remaining piece should be 1 BTC
assertEquals(result.get(1).getAmount().compareTo(new BigDecimal("1.0")), 0, String.format("%s != 1.0", result.get(0).getAmount()));
assertEquals(result.get(1).getCurrency(), Currency.BTC);
@@ -314,6 +338,11 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
assertEquals(result.get(1).getUsageName(), usage.getName());
assertTrue(result.get(1).getStartDate().compareTo(firstBCDDate) == 0);
assertTrue(result.get(1).getEndDate().compareTo(endDate) == 0);
+
+ // check item detail
+ List<UsageInArrearDetail> itemDetails2 = objectMapper.readValue(result.get(1).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ assertEquals(itemDetails2.size(), 1);
+ assertEquals(itemDetails2.get(0).getAmount().compareTo(result.get(1).getAmount()),0);
}
@Test(groups = "fast")
@@ -407,7 +436,7 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
final BillingEvent event2 = createMockBillingEvent(new LocalDate(2014, 10, 16).toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.<Usage>emptyList());
- final ContiguousIntervalUsageInArrear intervalConsumableInArrear = new ContiguousIntervalUsageInArrear(usage, accountId, invoiceId, rawUsages, targetDate, rawUsageStartDate, internalCallContext);
+ final ContiguousIntervalUsageInArrear intervalConsumableInArrear = new ContiguousIntervalUsageInArrear(usage, accountId, invoiceId, rawUsages, targetDate, rawUsageStartDate, usageDetailMode, internalCallContext);
intervalConsumableInArrear.addBillingEvent(event1);
intervalConsumableInArrear.addBillingEvent(event2);
@@ -415,4 +444,365 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
assertEquals(res.getTransitionTimes().size(), 0);
}
+ @Test(groups = "fast")
+ public void testTobeBilledForUnit() throws CatalogApiException {
+
+ final DefaultTieredBlock block1 = createDefaultTieredBlock("cell-phone-minutes", 1000, 10000, new BigDecimal("0.5"));
+ final DefaultTieredBlock block2 = createDefaultTieredBlock("Mbytes", 512, 512000, new BigDecimal("0.3"));
+ final DefaultTier tier = createDefaultTierWithBlocks(block1, block2);
+
+ final DefaultUsage usage = createConsumableInArrearUsage(usageName, BillingPeriod.MONTHLY, TierBlockPolicy.ALL_TIERS, tier);
+ final LocalDate targetDate = new LocalDate(2014, 03, 20);
+ final ContiguousIntervalUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
+ createMockBillingEvent(targetDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),
+ BillingPeriod.MONTHLY,
+ Collections.<Usage>emptyList())
+ );
+ List<UsageInArrearDetail> results = Lists.newArrayList();
+ results.addAll(intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("cell-phone-minutes", 1000L)));
+ results.addAll(intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("Mbytes", 30720L)));
+ assertEquals(results.size(), 2);
+
+ assertEquals(intervalConsumableInArrear.toBeBilledForUnit(results), new BigDecimal("18.5"));
+ }
+
+ @Test(groups = "fast")
+ public void testComputeMissingItemsAggregateModeAllTier() throws CatalogApiException, IOException {
+
+ // Case 1
+ List<RawUsage> rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 99L));
+
+ List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.AGGREGATE);
+ assertEquals(result.size(), 1);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("203")),0);
+
+ List<UsageInArrearDetail> itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ // BAR: 99 * 2 = 198
+ assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(0).getTier(), 1);
+ assertEquals(itemDetails.get(0).getAmount().compareTo(new BigDecimal("198")), 0);
+ assertEquals(itemDetails.get(0).getQuantity().intValue(), 99);
+ assertEquals(itemDetails.get(0).getTierPrice().compareTo(new BigDecimal("2.0")), 0);
+ // FOO: 5 * 1 = 5
+ assertEquals(itemDetails.get(1).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(1).getTier(), 1);
+ assertEquals(itemDetails.get(1).getAmount().compareTo(new BigDecimal("5")), 0);
+ assertEquals(itemDetails.get(1).getQuantity().intValue(), 5);
+ assertEquals(itemDetails.get(1).getTierPrice().compareTo(BigDecimal.ONE), 0);
+
+ // Case 2
+ rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
+
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.AGGREGATE);
+ assertEquals(result.size(), 1);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("225")),0);
+
+ itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ // BAR: 100 * 2 = 200
+ assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(0).getTier(), 1);
+ assertEquals(itemDetails.get(0).getAmount().compareTo(new BigDecimal("200.0")), 0);
+ assertEquals(itemDetails.get(0).getQuantity().intValue(), 100);
+ assertEquals(itemDetails.get(0).getTierPrice().compareTo(new BigDecimal("2.0")), 0);
+ // BAR: 1 * 20 = 20
+ assertEquals(itemDetails.get(1).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(1).getTier(), 2);
+ assertEquals(itemDetails.get(1).getAmount().compareTo(new BigDecimal("20.0")), 0);
+ assertEquals(itemDetails.get(1).getQuantity().intValue(), 1);
+ assertEquals(itemDetails.get(1).getTierPrice().compareTo(new BigDecimal("20.0")), 0);
+ // FOO: 5 * 1 = 5
+ assertEquals(itemDetails.get(2).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(2).getTier(), 1);
+ assertEquals(itemDetails.get(2).getAmount().compareTo(new BigDecimal("5")), 0);
+ assertEquals(itemDetails.get(2).getQuantity().intValue(), 5);
+ assertEquals(itemDetails.get(2).getTierPrice().compareTo(BigDecimal.ONE), 0);
+
+ // Case 3
+ rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 75L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
+
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.AGGREGATE);
+ assertEquals(result.size(), 1);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("2230")),0);
+
+ itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ // BAR: 100 * 2 = 200
+ assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(0).getTier(), 1);
+ assertEquals(itemDetails.get(0).getAmount().compareTo(new BigDecimal("200.0")), 0);
+ assertEquals(itemDetails.get(0).getQuantity().intValue(), 100);
+ assertEquals(itemDetails.get(0).getTierPrice().compareTo(new BigDecimal("2.0")), 0);
+ // BAR: 1 * 20 = 20
+ assertEquals(itemDetails.get(1).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(1).getTier(), 2);
+ assertEquals(itemDetails.get(1).getAmount().compareTo(new BigDecimal("20.0")), 0);
+ assertEquals(itemDetails.get(1).getQuantity().intValue(), 1);
+ assertEquals(itemDetails.get(1).getTierPrice().compareTo(new BigDecimal("20.0")), 0);
+ // FOO: 10 * 1 = 10
+ assertEquals(itemDetails.get(2).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(2).getTier(), 1);
+ assertEquals(itemDetails.get(2).getAmount().compareTo(BigDecimal.TEN), 0);
+ assertEquals(itemDetails.get(2).getQuantity().intValue(), 10);
+ assertEquals(itemDetails.get(2).getTierPrice().compareTo(BigDecimal.ONE), 0);
+ // FOO: 50 * 10 = 500
+ assertEquals(itemDetails.get(3).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(3).getTier(), 2);
+ assertEquals(itemDetails.get(3).getAmount().compareTo(new BigDecimal("500")), 0);
+ assertEquals(itemDetails.get(3).getQuantity().intValue(), 50);
+ assertEquals(itemDetails.get(3).getTierPrice().compareTo(BigDecimal.TEN), 0);
+ // FOO: 15 * 100 = 1500
+ assertEquals(itemDetails.get(4).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(4).getTier(), 3);
+ assertEquals(itemDetails.get(4).getAmount().compareTo(new BigDecimal("1500")), 0);
+ assertEquals(itemDetails.get(4).getQuantity().intValue(), 15);
+ assertEquals(itemDetails.get(4).getTierPrice().compareTo(new BigDecimal("100.0")), 0);
+ }
+
+ @Test(groups = "fast")
+ public void testComputeMissingItemsDetailModeAllTier() throws CatalogApiException, IOException {
+
+ // Case 1
+ List<RawUsage> rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 99L));
+
+ List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.DETAIL);
+ assertEquals(result.size(), 2);
+ // BAR: 99 * 2 = 198
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("198")), 0);
+ assertEquals(result.get(0).getQuantity().intValue(), 99);
+ assertEquals(result.get(0).getRate().compareTo(new BigDecimal("2.0")), 0);
+ // FOO: 5 * 1 = 5
+ assertEquals(result.get(1).getAmount().compareTo(new BigDecimal("5")), 0);
+ assertEquals(result.get(1).getQuantity().intValue(), 5);
+ assertEquals(result.get(1).getRate().compareTo(BigDecimal.ONE), 0);
+
+ // Case 2
+ rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
+
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.DETAIL);
+ assertEquals(result.size(), 3);
+ // BAR: 100 * 2 = 200
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("200.0")), 0);
+ assertEquals(result.get(0).getQuantity().intValue(), 100);
+ assertEquals(result.get(0).getRate().compareTo(new BigDecimal("2.0")), 0);
+ // BAR: 1 * 20 = 20
+ assertEquals(result.get(1).getAmount().compareTo(new BigDecimal("20.0")), 0);
+ assertEquals(result.get(1).getQuantity().intValue(), 1);
+ assertEquals(result.get(1).getRate().compareTo(new BigDecimal("20.0")), 0);
+ // FOO: 5 * 1 = 5
+ assertEquals(result.get(2).getAmount().compareTo(new BigDecimal("5")), 0);
+ assertEquals(result.get(2).getQuantity().intValue(), 5);
+ assertEquals(result.get(2).getRate().compareTo(BigDecimal.ONE), 0);
+
+ // Case 3
+ rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 75L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
+
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.DETAIL);
+ assertEquals(result.size(), 5);
+ // BAR: 100 * 2 = 200
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("200.0")), 0);
+ assertEquals(result.get(0).getQuantity().intValue(), 100);
+ assertEquals(result.get(0).getRate().compareTo(new BigDecimal("2.0")), 0);
+ // BAR: 1 * 20 = 20
+ assertEquals(result.get(1).getAmount().compareTo(new BigDecimal("20.0")), 0);
+ assertEquals(result.get(1).getQuantity().intValue(), 1);
+ assertEquals(result.get(1).getRate().compareTo(new BigDecimal("20.0")), 0);
+ // FOO: 10 * 1 = 10
+ assertEquals(result.get(2).getAmount().compareTo(BigDecimal.TEN), 0);
+ assertEquals(result.get(2).getQuantity().intValue(), 10);
+ assertEquals(result.get(2).getRate().compareTo(BigDecimal.ONE), 0);
+ // FOO: 50 * 10 = 500
+ assertEquals(result.get(3).getAmount().compareTo(new BigDecimal("500")), 0);
+ assertEquals(result.get(3).getQuantity().intValue(), 50);
+ assertEquals(result.get(3).getRate().compareTo(BigDecimal.TEN), 0);
+ // FOO: 15 * 100 = 1500
+ assertEquals(result.get(4).getAmount().compareTo(new BigDecimal("1500")), 0);
+ assertEquals(result.get(4).getQuantity().intValue(), 15);
+ assertEquals(result.get(4).getRate().compareTo(new BigDecimal("100.0")), 0);
+ }
+
+ @Test(groups = "fast")
+ public void testComputeMissingItemsAggregateModeTopTier() throws CatalogApiException, IOException {
+
+ // Case 1
+ List<RawUsage> rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 99L));
+
+ List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.AGGREGATE);
+ assertEquals(result.size(), 1);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("203")),0);
+
+ List<UsageInArrearDetail> itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ // BAR: 99 * 2 = 198
+ assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(0).getTier(), 1);
+ assertEquals(itemDetails.get(0).getAmount().compareTo(new BigDecimal("198")), 0);
+ assertEquals(itemDetails.get(0).getQuantity().intValue(), 99);
+ assertEquals(itemDetails.get(0).getTierPrice().compareTo(new BigDecimal("2.0")), 0);
+ // FOO: 5 * 1 = 5
+ assertEquals(itemDetails.get(1).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(1).getTier(), 1);
+ assertEquals(itemDetails.get(1).getAmount().compareTo(new BigDecimal("5")), 0);
+ assertEquals(itemDetails.get(1).getQuantity().intValue(), 5);
+ assertEquals(itemDetails.get(1).getTierPrice().compareTo(BigDecimal.ONE), 0);
+
+ // Case 2
+ rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
+
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.AGGREGATE);
+ assertEquals(result.size(), 1);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("2025")),0);
+
+ itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ // BAR: 101 * 20 = 2020
+ assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(0).getTier(), 2);
+ assertEquals(itemDetails.get(0).getAmount().compareTo(new BigDecimal("2020.0")), 0);
+ assertEquals(itemDetails.get(0).getQuantity().intValue(), 101);
+ assertEquals(itemDetails.get(0).getTierPrice().compareTo(new BigDecimal("20.0")), 0);
+ // FOO: 5 * 1 = 5
+ assertEquals(itemDetails.get(1).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(1).getTier(), 1);
+ assertEquals(itemDetails.get(1).getAmount().compareTo(new BigDecimal("5")), 0);
+ assertEquals(itemDetails.get(1).getQuantity().intValue(), 5);
+ assertEquals(itemDetails.get(1).getTierPrice().compareTo(BigDecimal.ONE), 0);
+
+ // Case 3
+ rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 76L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
+
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.AGGREGATE);
+ assertEquals(result.size(), 1);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("9620")),0);
+
+ itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ // BAR: 101 * 20 = 2020
+ assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(0).getTier(), 2);
+ assertEquals(itemDetails.get(0).getAmount().compareTo(new BigDecimal("2020.0")), 0);
+ assertEquals(itemDetails.get(0).getQuantity().intValue(), 101);
+ assertEquals(itemDetails.get(0).getTierPrice().compareTo(new BigDecimal("20.0")), 0);
+ // FOO: 76 * 100 = 7500
+ assertEquals(itemDetails.get(1).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(1).getTier(), 3);
+ assertEquals(itemDetails.get(1).getAmount().compareTo(new BigDecimal("7600")), 0);
+ assertEquals(itemDetails.get(1).getQuantity().intValue(), 76);
+ assertEquals(itemDetails.get(1).getTierPrice().compareTo(new BigDecimal("100.0")), 0);
+ }
+
+ @Test(groups = "fast")
+ public void testComputeMissingItemsDetailModeTopTier() throws CatalogApiException, IOException {
+
+ // Case 1
+ List<RawUsage> rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 99L));
+
+ List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.DETAIL);
+ assertEquals(result.size(), 2);
+ // BAR: 99 * 2 = 198
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("198")), 0);
+ assertEquals(result.get(0).getQuantity().intValue(), 99);
+ assertEquals(result.get(0).getRate().compareTo(new BigDecimal("2.0")), 0);
+ // FOO: 5 * 1 = 5
+ assertEquals(result.get(1).getAmount().compareTo(new BigDecimal("5")), 0);
+ assertEquals(result.get(1).getQuantity().intValue(), 5);
+ assertEquals(result.get(1).getRate().compareTo(BigDecimal.ONE), 0);
+
+ // Case 2
+ rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
+
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.DETAIL);
+ assertEquals(result.size(), 2);
+ // BAR: 101 * 20 = 2020
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("2020.0")), 0);
+ assertEquals(result.get(0).getQuantity().intValue(), 101);
+ assertEquals(result.get(0).getRate().compareTo(new BigDecimal("20.0")), 0);
+ // FOO: 5 * 1 = 5
+ assertEquals(result.get(1).getAmount().compareTo(new BigDecimal("5")), 0);
+ assertEquals(result.get(1).getQuantity().intValue(), 5);
+ assertEquals(result.get(1).getRate().compareTo(BigDecimal.ONE), 0);
+
+ // Case 3
+ rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 76L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
+
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.DETAIL);
+ assertEquals(result.size(), 2);
+ // BAR: 101 * 20 = 2020
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("2020.0")), 0);
+ assertEquals(result.get(0).getQuantity().intValue(), 101);
+ assertEquals(result.get(0).getRate().compareTo(new BigDecimal("20.0")), 0);
+ // FOO: 76 * 100 = 7500
+ assertEquals(result.get(1).getAmount().compareTo(new BigDecimal("7600")), 0);
+ assertEquals(result.get(1).getQuantity().intValue(), 76);
+ assertEquals(result.get(1).getRate().compareTo(new BigDecimal("100.0")), 0);
+ }
+
+ private List<InvoiceItem> produceInvoiceItems(List<RawUsage> rawUsages, TierBlockPolicy tierBlockPolicy, UsageDetailMode usageDetailMode) throws CatalogApiException {
+
+ final LocalDate startDate = new LocalDate(2014, 03, 20);
+ final LocalDate firstBCDDate = new LocalDate(2014, 04, 15);
+ final LocalDate endDate = new LocalDate(2014, 05, 15);
+
+ final DefaultTieredBlock blockFooTier1 = createDefaultTieredBlock("FOO", 1, 10, BigDecimal.ONE);
+ final DefaultTieredBlock blockBarTier1 = createDefaultTieredBlock("BAR", 1, 100, new BigDecimal("2"));
+ final DefaultTier tier1 = createDefaultTierWithBlocks(blockFooTier1, blockBarTier1);
+
+ final DefaultTieredBlock blockFooTier2 = createDefaultTieredBlock("FOO", 1, 50, BigDecimal.TEN);
+ final DefaultTieredBlock blockBarTier2 = createDefaultTieredBlock("BAR", 1, 500, new BigDecimal("20"));
+ final DefaultTier tier2 = createDefaultTierWithBlocks(blockFooTier2, blockBarTier2);
+
+ final DefaultTieredBlock blockFooTier3 = createDefaultTieredBlock("FOO", 1, 75, new BigDecimal("100"));
+ final DefaultTieredBlock blockBarTier3 = createDefaultTieredBlock("BAR", 1, 750, new BigDecimal("200"));
+ final DefaultTier tier3 = createDefaultTierWithBlocks(blockFooTier3, blockBarTier3);
+ final DefaultUsage usage = createConsumableInArrearUsage(usageName, BillingPeriod.MONTHLY, tierBlockPolicy, tier1, tier2, tier3);
+
+ final LocalDate targetDate = endDate;
+
+ final BillingEvent event1 = createMockBillingEvent(startDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),BillingPeriod.MONTHLY, Collections.<Usage>emptyList());
+ final BillingEvent event2 = createMockBillingEvent(endDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.<Usage>emptyList());
+
+ final ContiguousIntervalUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, rawUsages, targetDate, true, usageDetailMode, event1, event2);
+
+ final UsageInArrearItemsAndNextNotificationDate usageResult = intervalConsumableInArrear.computeMissingItemsAndNextNotificationDate(ImmutableList.<InvoiceItem>of());
+ final List<InvoiceItem> rawResults = usageResult.getInvoiceItems();
+ final List<InvoiceItem> result = ImmutableList.copyOf(Iterables.filter(rawResults, new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ return input.getAmount().compareTo(BigDecimal.ZERO) > 0;
+ }
+ }));
+
+ for (InvoiceItem item: result) {
+ assertEquals(item.getCurrency(), Currency.BTC);
+ assertEquals(item.getAccountId(), accountId);
+ assertEquals(item.getBundleId(), bundleId);
+ assertEquals(item.getSubscriptionId(), subscriptionId);
+ assertEquals(item.getPlanName(), planName);
+ assertEquals(item.getPhaseName(), phaseName);
+ assertEquals(item.getUsageName(), usage.getName());
+ assertTrue(item.getStartDate().compareTo(startDate) == 0);
+ assertTrue(item.getEndDate().compareTo(firstBCDDate) == 0);
+ }
+
+ return result;
+ }
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestSubscriptionConsumableInArrear.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestSubscriptionConsumableInArrear.java
index eb02634..17831a6 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestSubscriptionConsumableInArrear.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestSubscriptionConsumableInArrear.java
@@ -75,7 +75,7 @@ public class TestSubscriptionConsumableInArrear extends TestUsageInArrearBase {
LocalDate targetDate = new LocalDate(2013, 6, 23);
- final SubscriptionUsageInArrear foo = new SubscriptionUsageInArrear(accountId, invoiceId, billingEvents, ImmutableList.<RawUsage>of(), targetDate, new LocalDate(dt1, DateTimeZone.UTC), internalCallContext);
+ final SubscriptionUsageInArrear foo = new SubscriptionUsageInArrear(accountId, invoiceId, billingEvents, ImmutableList.<RawUsage>of(), targetDate, new LocalDate(dt1, DateTimeZone.UTC), usageDetailMode, internalCallContext);
final List<ContiguousIntervalUsageInArrear> result = foo.computeInArrearUsageInterval();
assertEquals(result.size(), 3);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java
index 0c1f557..f5c5c60 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java
@@ -44,9 +44,12 @@ import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.usage.RawUsage;
+import org.killbill.billing.util.config.definition.InvoiceConfig.UsageDetailMode;
import org.mockito.Mockito;
import org.testng.annotations.BeforeClass;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
public abstract class TestUsageInArrearBase extends InvoiceTestSuiteNoDB {
protected int BCD;
@@ -58,6 +61,8 @@ public abstract class TestUsageInArrearBase extends InvoiceTestSuiteNoDB {
protected String phaseName;
protected Currency currency;
protected String usageName;
+ protected ObjectMapper objectMapper;
+
@BeforeClass(groups = "fast")
protected void beforeClass() throws Exception {
@@ -71,10 +76,16 @@ public abstract class TestUsageInArrearBase extends InvoiceTestSuiteNoDB {
planName = "planName";
phaseName = "phaseName";
currency = Currency.BTC;
+ usageDetailMode = invoiceConfig.getItemResultBehaviorMode(internalCallContext);
+ objectMapper = new ObjectMapper();
}
protected ContiguousIntervalUsageInArrear createContiguousIntervalConsumableInArrear(final DefaultUsage usage, final List<RawUsage> rawUsages, final LocalDate targetDate, final boolean closedInterval, final BillingEvent... events) {
- final ContiguousIntervalUsageInArrear intervalConsumableInArrear = new ContiguousIntervalUsageInArrear(usage, accountId, invoiceId, rawUsages, targetDate, new LocalDate(events[0].getEffectiveDate()), internalCallContext);
+ return createContiguousIntervalConsumableInArrear(usage, rawUsages, targetDate, closedInterval, usageDetailMode, events);
+ }
+
+ protected ContiguousIntervalUsageInArrear createContiguousIntervalConsumableInArrear(final DefaultUsage usage, final List<RawUsage> rawUsages, final LocalDate targetDate, final boolean closedInterval, UsageDetailMode detailMode, final BillingEvent... events) {
+ final ContiguousIntervalUsageInArrear intervalConsumableInArrear = new ContiguousIntervalUsageInArrear(usage, accountId, invoiceId, rawUsages, targetDate, new LocalDate(events[0].getEffectiveDate()), detailMode, internalCallContext);
for (final BillingEvent event : events) {
intervalConsumableInArrear.addBillingEvent(event);
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceItemJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceItemJson.java
index 8ad9cc1..fba3f1e 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceItemJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceItemJson.java
@@ -63,7 +63,10 @@ public class InvoiceItemJson extends JsonBase {
private final LocalDate startDate;
private final LocalDate endDate;
private final BigDecimal amount;
+ private final BigDecimal rate;
private final String currency;
+ private final Integer quantity;
+ private final String itemDetails;
private List<InvoiceItemJson> childItems;
@JsonCreator
@@ -85,7 +88,10 @@ public class InvoiceItemJson extends JsonBase {
@JsonProperty("startDate") final LocalDate startDate,
@JsonProperty("endDate") final LocalDate endDate,
@JsonProperty("amount") final BigDecimal amount,
+ @JsonProperty("rate") final BigDecimal rate,
@JsonProperty("currency") final String currency,
+ @JsonProperty("quantity") final Integer quantity,
+ @JsonProperty("itemDetails") final String itemDetails,
@JsonProperty("childItems") final List<InvoiceItemJson> childItems,
@JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
super(auditLogs);
@@ -107,8 +113,11 @@ public class InvoiceItemJson extends JsonBase {
this.startDate = startDate;
this.endDate = endDate;
this.amount = amount;
+ this.rate = rate;
this.currency = currency;
this.childItems = childItems;
+ this.quantity = quantity;
+ this.itemDetails = itemDetails;
}
public InvoiceItemJson(final InvoiceItem item, final List<InvoiceItem> childItems, @Nullable final List<AuditLog> auditLogs) {
@@ -118,7 +127,8 @@ public class InvoiceItemJson extends JsonBase {
item.getPrettyPlanName(), item.getPrettyPhaseName(), item.getPrettyUsageName(),
item.getInvoiceItemType().toString(),
item.getDescription(), item.getStartDate(), item.getEndDate(),
- item.getAmount(), item.getCurrency().name(), toInvoiceItemJson(childItems), toAuditLogJson(auditLogs));
+ item.getAmount(), item.getRate(), item.getCurrency().name(),
+ item.getQuantity(), item.getItemDetails(), toInvoiceItemJson(childItems), toAuditLogJson(auditLogs));
}
private static List<InvoiceItemJson> toInvoiceItemJson(final List<InvoiceItem> childItems) {
@@ -222,7 +232,7 @@ public class InvoiceItemJson extends JsonBase {
@Override
public BigDecimal getRate() {
- return null;
+ return rate;
}
@Override
@@ -231,6 +241,12 @@ public class InvoiceItemJson extends JsonBase {
}
@Override
+ public Integer getQuantity() { return quantity; }
+
+ @Override
+ public String getItemDetails() { return itemDetails; }
+
+ @Override
public boolean matches(final Object o) {
return false;
}
@@ -328,6 +344,8 @@ public class InvoiceItemJson extends JsonBase {
return amount;
}
+ public BigDecimal getRate() { return rate; }
+
public String getCurrency() {
return currency;
}
@@ -336,6 +354,10 @@ public class InvoiceItemJson extends JsonBase {
return childItems;
}
+ public Integer getQuantity() { return quantity; }
+
+ public String getItemDetails() { return itemDetails; }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
@@ -354,7 +376,10 @@ public class InvoiceItemJson extends JsonBase {
sb.append(", startDate=").append(startDate);
sb.append(", endDate=").append(endDate);
sb.append(", amount=").append(amount);
+ sb.append(", rate=").append(rate);
sb.append(", currency=").append(currency);
+ sb.append(", quantity=").append(quantity);
+ sb.append(", itemDetails=").append(itemDetails);
sb.append(", childItems=").append(childItems);
sb.append('}');
return sb.toString();
@@ -421,6 +446,15 @@ public class InvoiceItemJson extends JsonBase {
if (childItems != null ? !childItems.equals(that.childItems) : that.childItems != null) {
return false;
}
+ if (quantity != null ? !quantity.equals(that.quantity) : that.quantity != null) {
+ return false;
+ }
+ if (itemDetails != null ? !itemDetails.equals(that.itemDetails) : that.itemDetails != null) {
+ return false;
+ }
+ if (rate != null ? rate.compareTo(that.rate) != 0 : that.rate != null) {
+ return false;
+ }
return true;
}
@@ -441,7 +475,10 @@ public class InvoiceItemJson extends JsonBase {
result = 31 * result + (startDate != null ? startDate.hashCode() : 0);
result = 31 * result + (endDate != null ? endDate.hashCode() : 0);
result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (rate != null ? rate.hashCode() : 0);
result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (quantity != null ? quantity.hashCode() : 0);
+ result = 31 * result + (itemDetails != null ? itemDetails.hashCode() : 0);
result = 31 * result + (childItems != null ? childItems.hashCode() : 0);
return result;
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
index 2bb186c..f98623f 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
@@ -576,7 +576,10 @@ public class InvoiceResource extends JaxRsResourceBase {
input.getStartDate(),
input.getEndDate(),
input.getAmount(),
+ input.getRate(),
accountCurrency.name(),
+ input.getQuantity(),
+ input.getItemDetails(),
null,
null);
}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceItemJsonSimple.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceItemJsonSimple.java
index 42f51ba..f61257d 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceItemJsonSimple.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceItemJsonSimple.java
@@ -56,7 +56,7 @@ public class TestInvoiceItemJsonSimple extends JaxrsTestSuiteNoDB {
bundleId, subscriptionId, planName, phaseName, usageName,
null, null, null,
type, description,
- startDate, endDate, amount, currency.name(), null, auditLogs);
+ startDate, endDate, amount, null, currency.name(), null, null, null, auditLogs);
Assert.assertEquals(invoiceItemJson.getInvoiceItemId(), invoiceItemId);
Assert.assertEquals(invoiceItemJson.getInvoiceId(), invoiceId);
Assert.assertEquals(invoiceItemJson.getLinkedInvoiceItemId(), linkedInvoiceItemId);
diff --git a/payment/src/test/java/org/killbill/billing/payment/MockRecurringInvoiceItem.java b/payment/src/test/java/org/killbill/billing/payment/MockRecurringInvoiceItem.java
index fa16ee5..1627a6d 100644
--- a/payment/src/test/java/org/killbill/billing/payment/MockRecurringInvoiceItem.java
+++ b/payment/src/test/java/org/killbill/billing/payment/MockRecurringInvoiceItem.java
@@ -42,6 +42,8 @@ public class MockRecurringInvoiceItem extends EntityBase implements InvoiceItem
protected final BigDecimal amount;
protected final Currency currency;
protected final String usageName;
+ protected final Integer quantity;
+ protected final String itemDetails;
public MockRecurringInvoiceItem(final UUID invoiceId, final UUID accountId, final UUID bundleId, final UUID subscriptionId,
final String planName, final String phaseName, final String usageName, final LocalDate startDate, final LocalDate endDate,
@@ -52,12 +54,12 @@ public class MockRecurringInvoiceItem extends EntityBase implements InvoiceItem
public MockRecurringInvoiceItem(final UUID invoiceId, final UUID accountId, final UUID bundleId, final UUID subscriptionId, final String planName, final String phaseName, final String usageName,
final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency, final BigDecimal rate, final UUID reversedItemId) {
this(UUID.randomUUID(), invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usageName,
- startDate, endDate, amount, currency, rate, reversedItemId);
+ startDate, endDate, amount, currency, rate, reversedItemId, null, null);
}
public MockRecurringInvoiceItem(final UUID id, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId, @Nullable final UUID subscriptionId, final String planName, final String phaseName,
final String usageName, final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency,
- final BigDecimal rate, final UUID reversedItemId) {
+ final BigDecimal rate, final UUID reversedItemId, final Integer quantity, final String itemDetails) {
super(id);
this.invoiceId = invoiceId;
this.accountId = accountId;
@@ -72,6 +74,8 @@ public class MockRecurringInvoiceItem extends EntityBase implements InvoiceItem
this.currency = currency;
this.rate = rate;
this.reversedItemId = reversedItemId;
+ this.quantity = quantity;
+ this.itemDetails = itemDetails;
}
@Override
@@ -165,6 +169,12 @@ public class MockRecurringInvoiceItem extends EntityBase implements InvoiceItem
}
@Override
+ public Integer getQuantity() { return quantity; }
+
+ @Override
+ public String getItemDetails() { return itemDetails; }
+
+ @Override
public boolean matches(final Object other) {
throw new UnsupportedOperationException();
}
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index 0b04383..a3df44f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>killbill-oss-parent</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.141.26</version>
+ <version>0.141.27</version>
</parent>
<artifactId>killbill</artifactId>
<version>0.19.3-SNAPSHOT</version>
diff --git a/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java b/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
index 65ea53b..2291565 100644
--- a/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
@@ -28,6 +28,11 @@ import org.skife.config.TimeSpan;
public interface InvoiceConfig extends KillbillConfig {
+ public enum UsageDetailMode {
+ AGGREGATE,
+ DETAIL,
+ }
+
@Config("org.killbill.invoice.maxNumberOfMonthsInFuture")
@Default("36")
@Description("Maximum target date to consider when generating an invoice")
@@ -117,4 +122,14 @@ public interface InvoiceConfig extends KillbillConfig {
@Default("true")
@Description("Whether the invoicing system is enabled")
boolean isInvoicingSystemEnabled(@Param("dummy") final InternalTenantContext tenantContext);
+
+ @Config("org.killbill.invoice.item.result.behavior.mode")
+ @Default("AGGREGATE")
+ @Description("How the result for an item will be reported (aggregate mode or detail mode). ")
+ UsageDetailMode getItemResultBehaviorMode();
+
+ @Config("org.killbill.invoice.item.result.behavior.mode")
+ @Default("AGGREGATE")
+ @Description("How the result for an item will be reported (aggregate mode or detail mode). ")
+ UsageDetailMode getItemResultBehaviorMode(@Param("dummy") final InternalTenantContext tenantContext);
}