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..8285bc5 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
@@ -19,6 +19,7 @@ package org.killbill.billing.invoice.usage;
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -30,6 +31,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.CatalogApiException;
@@ -50,6 +52,8 @@ import org.killbill.billing.usage.api.RolledUpUsage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
@@ -184,21 +188,24 @@ public class ContiguousIntervalUsageInArrear {
final List<RolledUpUsage> allUsage = getRolledUpUsage();
for (final RolledUpUsage ru : allUsage) {
+ List<ToBeBilledConsumableInArrearDetail> toBeBilledUsageDetails = Lists.newLinkedList();
BigDecimal toBeBilledUsage = BigDecimal.ZERO;
if (usage.getUsageType() == UsageType.CAPACITY) {
toBeBilledUsage = computeToBeBilledCapacityInArrear(ru.getRolledUpUnits());
} else /* UsageType.CONSUMABLE */{
// Compute total price amount that should be billed for that period of time (and usage section) across unitTypes.
+ int tierNum = 1;
for (final RolledUpUnit cur : ru.getRolledUpUnits()) {
if (!unitTypes.contains(cur.getUnitType())) {
log.warn("ContiguousIntervalConsumableInArrear is skipping unitType " + cur.getUnitType());
continue;
}
- final BigDecimal toBeBilledForUnit = computeToBeBilledConsumableInArrear(cur);
- toBeBilledUsage = toBeBilledUsage.add(toBeBilledForUnit);
+ toBeBilledUsageDetails.addAll(computeToBeBilledConsumableInArrear(cur, tierNum++));
}
+ toBeBilledUsage = toBeBilledUsage.add(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);
@@ -207,9 +214,12 @@ public class ContiguousIntervalUsageInArrear {
// Compare the two and add the missing piece if required.
if (!billedItems.iterator().hasNext() || billedUsage.compareTo(toBeBilledUsage) < 0) {
final BigDecimal amountToBill = toBeBilledUsage.subtract(billedUsage);
+ if (billedUsage.compareTo(BigDecimal.ZERO) > 0){
+ toBeBilledUsageDetails.add(new ToBeBilledConsumableInArrearDetail(-1, "billedUsage", billedUsage, -1));
+ }
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());
+ getPhaseName(), usage.getName(), ru.getStart(), ru.getEnd(), amountToBill, getCurrency(),null,toJson(toBeBilledUsageDetails));
result.add(item);
}
}
@@ -394,24 +404,25 @@ public class ContiguousIntervalUsageInArrear {
* @throws CatalogApiException
*/
@VisibleForTesting
- BigDecimal computeToBeBilledConsumableInArrear(final RolledUpUnit roUnit) throws CatalogApiException {
+ List<ToBeBilledConsumableInArrearDetail> computeToBeBilledConsumableInArrear(final RolledUpUnit roUnit, int tierNum) throws CatalogApiException {
Preconditions.checkState(isBuilt.get());
final List<TieredBlock> tieredBlocks = getConsumableInArrearTieredBlocks(usage, roUnit.getUnitType());
switch (usage.getTierBlockPolicy()) {
case ALL_TIERS:
- return computeToBeBilledConsumableInArrearWith_ALL_TIERS(tieredBlocks, roUnit.getAmount());
+ return computeToBeBilledConsumableInArrearWith_ALL_TIERS(tieredBlocks, roUnit.getAmount(), tierNum);
case TOP_TIER:
- return computeToBeBilledConsumableInArrearWith_TOP_TIER(tieredBlocks, roUnit.getAmount());
+ return Arrays.asList(computeToBeBilledConsumableInArrearWith_TOP_TIER(tieredBlocks, roUnit.getAmount(), tierNum));
default:
throw new IllegalStateException("Unknown TierBlockPolicy " + usage.getTierBlockPolicy());
}
}
- BigDecimal computeToBeBilledConsumableInArrearWith_ALL_TIERS(final List<TieredBlock> tieredBlocks, final Long units) throws CatalogApiException {
+ List<ToBeBilledConsumableInArrearDetail> computeToBeBilledConsumableInArrearWith_ALL_TIERS(final List<TieredBlock> tieredBlocks, final Long units, int tierNum) throws CatalogApiException {
+ List<ToBeBilledConsumableInArrearDetail> toBeBilledDetails = Lists.newLinkedList();
BigDecimal result = BigDecimal.ZERO;
int remainingUnits = units.intValue();
for (final TieredBlock tieredBlock : tieredBlocks) {
@@ -426,12 +437,12 @@ public class ContiguousIntervalUsageInArrear {
nbUsedTierBlocks = tmp;
remainingUnits = 0;
}
- result = result.add(tieredBlock.getPrice().getPrice(getCurrency()).multiply(new BigDecimal(nbUsedTierBlocks)));
+ toBeBilledDetails.add(new ToBeBilledConsumableInArrearDetail(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 {
+ ToBeBilledConsumableInArrearDetail computeToBeBilledConsumableInArrearWith_TOP_TIER(final List<TieredBlock> tieredBlocks, final Long units, int tierNum) throws CatalogApiException {
int remainingUnits = units.intValue();
@@ -451,7 +462,8 @@ public class ContiguousIntervalUsageInArrear {
}
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 ToBeBilledConsumableInArrearDetail(tierNum, targetBlock.getUnit().getName(), targetBlock.getPrice().getPrice(getCurrency()), nbBlocks);
}
@@ -557,4 +569,54 @@ public class ContiguousIntervalUsageInArrear {
return nextNotificationDate;
}
}
+
+ private BigDecimal toBeBilledForUnit(List<ToBeBilledConsumableInArrearDetail> toBeBilledDetails){
+ BigDecimal result = BigDecimal.ZERO;
+ for (ToBeBilledConsumableInArrearDetail toBeBilled: toBeBilledDetails){
+ result = result.add(toBeBilled.getAmount());
+ }
+ return result;
+ }
+
+ private String toJson(List<ToBeBilledConsumableInArrearDetail> toBeBilledConsumableInArrearDetails) throws CatalogApiException {
+ String result = null;
+ if (toBeBilledConsumableInArrearDetails != null && toBeBilledConsumableInArrearDetails.size() > 0){
+ ObjectMapper objectMapper = new ObjectMapper();
+ try {
+ result = objectMapper.writeValueAsString(toBeBilledConsumableInArrearDetails);
+ } catch (JsonProcessingException e) {
+ throw new CatalogApiException(e, ErrorCode.__UNKNOWN_ERROR_CODE);
+ }
+ }
+ return result;
+ }
+
+ public class ToBeBilledConsumableInArrearDetail {
+
+ private final int tier;
+ private final String tierUnit;
+ private final BigDecimal tierPrice;
+ private final int quantity;
+ private final BigDecimal amount;
+
+ public ToBeBilledConsumableInArrearDetail(int tier, String tierUnit, BigDecimal tierPrice, int quantity){
+ this(tier, tierUnit, tierPrice, quantity, tierPrice.multiply(new BigDecimal(quantity)));
+ }
+
+ public ToBeBilledConsumableInArrearDetail(int tier, String tierUnit, BigDecimal tierPrice, int quantity, BigDecimal amount){
+ this.tier = tier;
+ this.tierUnit = tierUnit;
+ this.tierPrice = tierPrice;
+ this.quantity = quantity;
+ this.amount = amount;
+ }
+
+ public int getTier() { return tier; };
+ public String getTierUnit() { return tierUnit; }
+ public BigDecimal getTierPrice() { return tierPrice; }
+ public int getQuantity() { return quantity; }
+ public BigDecimal getAmount() {
+ return amount;
+ }
+ }
}
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..d91f639 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,6 +18,7 @@
package org.killbill.billing.invoice.usage;
+import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
@@ -36,6 +37,7 @@ 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.ToBeBilledConsumableInArrearDetail;
import org.killbill.billing.invoice.usage.ContiguousIntervalUsageInArrear.UsageInArrearItemsAndNextNotificationDate;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.usage.RawUsage;
@@ -46,6 +48,10 @@ 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 +145,12 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
Collections.<Usage>emptyList())
);
- final BigDecimal result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 111L));
-
+ List<ToBeBilledConsumableInArrearDetail> result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 111L), 1);
+ 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 +173,12 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
Collections.<Usage>emptyList())
);
- final BigDecimal result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 5325L));
+ List<ToBeBilledConsumableInArrearDetail> result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 5325L),1);
+ 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 +207,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<ToBeBilledConsumableInArrearDetail> inputTier1 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 1000L), 1);
+ 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<ToBeBilledConsumableInArrearDetail> inputTier2 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 101000L), 1);
+ 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<ToBeBilledConsumableInArrearDetail> inputTier3 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 101001L), 1);
+ 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<ToBeBilledConsumableInArrearDetail> inputLastTier = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 300000L), 1);
+ 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 +252,18 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
Collections.<Usage>emptyList())
);
- final BigDecimal result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 111L));
+ List<ToBeBilledConsumableInArrearDetail> result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 111L),1);
+ 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,6 +305,7 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
}
}));
+ ObjectMapper objectMapper = new ObjectMapper();
// 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()));
@@ -303,6 +319,15 @@ 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<MockToBeBilledConsumableInArrearDetail> itemDetails = objectMapper.reader()
+ .forType(new TypeReference<List<MockToBeBilledConsumableInArrearDetail>>() {})
+ .readValue(result.get(0).getItemDetails());
+ assertEquals(itemDetails.size(), 2);
+ assertEquals(itemDetails.get(0).getAmount().compareTo(new BigDecimal("5")),0);
+ assertEquals(itemDetails.get(1).getAmount().compareTo(new BigDecimal("-1")),0);
+ assertEquals(result.get(0).getAmount().compareTo(itemDetails.get(0).getAmount().add(itemDetails.get(1).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 +339,16 @@ 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<MockToBeBilledConsumableInArrearDetail> itemDetails2 = objectMapper.reader()
+ .forType(new TypeReference<List<MockToBeBilledConsumableInArrearDetail>>() {})
+ .readValue(result.get(1).getItemDetails());
+ assertEquals(itemDetails2.size(), 2);
+ assertEquals(itemDetails2.get(0).getAmount().compareTo(new BigDecimal("2")),0);
+ assertEquals(itemDetails2.get(1).getAmount().compareTo(new BigDecimal("-1")),0);
+ assertEquals(result.get(1).getAmount().compareTo(itemDetails2.get(0).getAmount().add(itemDetails2.get(1).getAmount())),0);
+
}
@Test(groups = "fast")
@@ -415,4 +450,31 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
assertEquals(res.getTransitionTimes().size(), 0);
}
+ public static class MockToBeBilledConsumableInArrearDetail {
+
+ private int tier;
+ private String tierUnit;
+ private BigDecimal tierPrice;
+ private int quantity;
+ private BigDecimal amount;
+
+ @JsonCreator
+ public MockToBeBilledConsumableInArrearDetail(@JsonProperty("tier") int tier, @JsonProperty("tierUnit") String tierUnit,
+ @JsonProperty("tierPrice") BigDecimal tierPrice, @JsonProperty("quantity") int quantity,
+ @JsonProperty("amount") BigDecimal amount){
+ this.tier = tier;
+ this.tierUnit = tierUnit;
+ this.tierPrice = tierPrice;
+ this.quantity = quantity;
+ this.amount = amount;
+ }
+
+ public int getTier() { return tier; };
+ public String getTierUnit() { return tierUnit; }
+ public BigDecimal getTierPrice() { return tierPrice; }
+ public int getQuantity() { return quantity; }
+ public BigDecimal getAmount() {
+ return amount;
+ }
+ }
}