killbill-aplcache

Details

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/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java
index 726706c..b308c68 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
@@ -112,12 +112,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;
     }
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;
+        }
+    }
 }