killbill-aplcache

analytics: handle repair items as item adjustments Signed-off-by:

4/11/2013 5:46:58 PM

Details

diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessInvoiceDao.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessInvoiceDao.java
index 8e89f8e..023ba78 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessInvoiceDao.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessInvoiceDao.java
@@ -16,24 +16,32 @@
 
 package com.ning.billing.osgi.bundles.analytics.dao;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
 import javax.annotation.Nullable;
 
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.osgi.service.log.LogService;
 import org.skife.jdbi.v2.Transaction;
 import org.skife.jdbi.v2.TransactionStatus;
 
 import com.ning.billing.account.api.Account;
+import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.catalog.api.Plan;
 import com.ning.billing.catalog.api.PlanPhase;
 import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
 import com.ning.billing.osgi.bundles.analytics.AnalyticsRefreshException;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessAccountModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessInvoiceItemBaseModelDao;
@@ -46,6 +54,8 @@ import com.ning.killbill.osgi.libs.killbill.OSGIKillbillAPI;
 import com.ning.killbill.osgi.libs.killbill.OSGIKillbillDataSource;
 import com.ning.killbill.osgi.libs.killbill.OSGIKillbillLogService;
 
+import com.google.common.annotations.VisibleForTesting;
+
 public class BusinessInvoiceDao extends BusinessAnalyticsDaoBase {
 
     private final BusinessAccountDao businessAccountDao;
@@ -109,8 +119,11 @@ public class BusinessInvoiceDao extends BusinessAnalyticsDaoBase {
                                                                                         tenantRecordId,
                                                                                         reportGroup);
 
+            final List<InvoiceItem> allInvoiceItems = invoice.getInvoiceItems();
+            final Collection<InvoiceItem> sanitizedInvoiceItems = sanitizeInvoiceItems(allInvoiceItems);
+
             final List<BusinessInvoiceItemBaseModelDao> businessInvoiceItems = new ArrayList<BusinessInvoiceItemBaseModelDao>();
-            for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
+            for (final InvoiceItem invoiceItem : sanitizedInvoiceItems) {
                 final BusinessInvoiceItemBaseModelDao businessInvoiceItem = createBusinessInvoiceItem(account,
                                                                                                       invoice,
                                                                                                       invoiceItem,
@@ -205,4 +218,165 @@ public class BusinessInvoiceDao extends BusinessAnalyticsDaoBase {
                                                       tenantRecordId,
                                                       reportGroup);
     }
+
+    @VisibleForTesting
+    Collection<InvoiceItem> sanitizeInvoiceItems(final List<InvoiceItem> allInvoiceItems) {
+        // Build a convenience mapping between items -> repair_adj items (inverse of linkedItemId)
+        final Map<UUID, InvoiceItem> repairedInvoiceItemIdToRepairInvoiceItemMappings = new HashMap<UUID, InvoiceItem>();
+        for (final InvoiceItem invoiceItem : allInvoiceItems) {
+            if (InvoiceItemType.REPAIR_ADJ.equals(invoiceItem.getInvoiceItemType())) {
+                repairedInvoiceItemIdToRepairInvoiceItemMappings.put(invoiceItem.getLinkedItemId(), invoiceItem);
+            }
+        }
+
+        // Now find the "reparation" items, i.e. the ones which correspond to the repaired items
+        final Map<UUID, InvoiceItem> reparationInvoiceItemIdToRepairItemMappings = new LinkedHashMap<UUID, InvoiceItem>();
+        for (final InvoiceItem repairedInvoiceItem : allInvoiceItems) {
+            // Skip non-repaired items
+            if (!repairedInvoiceItemIdToRepairInvoiceItemMappings.keySet().contains(repairedInvoiceItem.getId())) {
+                continue;
+            }
+
+            InvoiceItem reparationItem = null;
+            for (final InvoiceItem invoiceItem : allInvoiceItems) {
+                // Try to find the matching "reparation" item
+                if (repairedInvoiceItem.getInvoiceItemType().equals(invoiceItem.getInvoiceItemType()) &&
+                    repairedInvoiceItem.getSubscriptionId().equals(invoiceItem.getSubscriptionId()) &&
+                    repairedInvoiceItem.getStartDate().compareTo(invoiceItem.getStartDate()) == 0 &&
+                    repairedInvoiceItem.getEndDate().isAfter(invoiceItem.getEndDate())) {
+                    if (reparationItem == null) {
+                        reparationItem = invoiceItem;
+                    } else {
+                        logService.log(LogService.LOG_ERROR, "Found multiple reparation items matching the repair item id " + repairedInvoiceItem.getId() + " - this should never happen!");
+                    }
+                }
+            }
+
+            if (reparationItem != null) {
+                reparationInvoiceItemIdToRepairItemMappings.put(reparationItem.getId(), repairedInvoiceItemIdToRepairInvoiceItemMappings.get(repairedInvoiceItem.getId()));
+            } else {
+                logService.log(LogService.LOG_ERROR, "Could not find the reparation item for the repair item id " + repairedInvoiceItem.getId() + " - this should never happen!");
+            }
+        }
+
+        // Filter the invoice items for analytics
+        final Collection<InvoiceItem> invoiceItemsForAnalytics = new LinkedList<InvoiceItem>();
+        for (final InvoiceItem invoiceItem : allInvoiceItems) {
+            if (InvoiceItemType.REPAIR_ADJ.equals(invoiceItem.getInvoiceItemType())) {
+                // We don't care, we'll create a special item for it below
+            } else if (reparationInvoiceItemIdToRepairItemMappings.keySet().contains(invoiceItem.getId())) {
+                // We do care - this is a reparation item. Create an item adjustment for it
+                final InvoiceItem repairInvoiceItem = reparationInvoiceItemIdToRepairItemMappings.get(invoiceItem.getId());
+                final InvoiceItem reparationInvoiceItem = invoiceItem;
+                invoiceItemsForAnalytics.add(new AdjustmentInvoiceItemForRepair(repairInvoiceItem, reparationInvoiceItem));
+            } else {
+                invoiceItemsForAnalytics.add(invoiceItem);
+            }
+        }
+
+        return invoiceItemsForAnalytics;
+    }
+
+    private class AdjustmentInvoiceItemForRepair implements InvoiceItem {
+
+        private final InvoiceItem repairInvoiceItem;
+        private final InvoiceItem reparationInvoiceItem;
+
+        private AdjustmentInvoiceItemForRepair(final InvoiceItem repairInvoiceItem,
+                                               final InvoiceItem reparationInvoiceItem) {
+            this.repairInvoiceItem = repairInvoiceItem;
+            this.reparationInvoiceItem = reparationInvoiceItem;
+        }
+
+        @Override
+        public InvoiceItemType getInvoiceItemType() {
+            return InvoiceItemType.ITEM_ADJ;
+        }
+
+        @Override
+        public UUID getInvoiceId() {
+            return repairInvoiceItem.getInvoiceId();
+        }
+
+        @Override
+        public UUID getAccountId() {
+            return repairInvoiceItem.getAccountId();
+        }
+
+        @Override
+        public LocalDate getStartDate() {
+            return repairInvoiceItem.getStartDate();
+        }
+
+        @Override
+        public LocalDate getEndDate() {
+            return repairInvoiceItem.getStartDate();
+        }
+
+        @Override
+        public BigDecimal getAmount() {
+            return reparationInvoiceItem.getAmount().add(repairInvoiceItem.getAmount());
+        }
+
+        @Override
+        public Currency getCurrency() {
+            return repairInvoiceItem.getCurrency();
+        }
+
+        @Override
+        public String getDescription() {
+            return null;
+        }
+
+        @Override
+        public UUID getBundleId() {
+            return null;
+        }
+
+        @Override
+        public UUID getSubscriptionId() {
+            return null;
+        }
+
+        @Override
+        public String getPlanName() {
+            return null;
+        }
+
+        @Override
+        public String getPhaseName() {
+            return null;
+        }
+
+        @Override
+        public BigDecimal getRate() {
+            return null;
+        }
+
+        @Override
+        public UUID getLinkedItemId() {
+            return repairInvoiceItem.getLinkedItemId();
+        }
+
+        @Override
+        public int compareTo(final InvoiceItem o) {
+            return repairInvoiceItem.compareTo(o);
+        }
+
+        @Override
+        public UUID getId() {
+            // Fake id, doesn't exist in raw tables
+            return UUID.randomUUID();
+        }
+
+        @Override
+        public DateTime getCreatedDate() {
+            return repairInvoiceItem.getCreatedDate();
+        }
+
+        @Override
+        public DateTime getUpdatedDate() {
+            return repairInvoiceItem.getUpdatedDate();
+        }
+    }
 }
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/model/BusinessModelDaoBase.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/model/BusinessModelDaoBase.java
index 8f4c2ff..7b8cfa1 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/model/BusinessModelDaoBase.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/model/BusinessModelDaoBase.java
@@ -148,9 +148,6 @@ public abstract class BusinessModelDaoBase {
 
         final BusinessModelDaoBase that = (BusinessModelDaoBase) o;
 
-        if (DEFAULT_REPORT_GROUP != null ? !DEFAULT_REPORT_GROUP.equals(that.DEFAULT_REPORT_GROUP) : that.DEFAULT_REPORT_GROUP != null) {
-            return false;
-        }
         if (accountExternalKey != null ? !accountExternalKey.equals(that.accountExternalKey) : that.accountExternalKey != null) {
             return false;
         }
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/AnalyticsTestSuiteNoDB.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/AnalyticsTestSuiteNoDB.java
index 97c0729..24423e1 100644
--- a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/AnalyticsTestSuiteNoDB.java
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/AnalyticsTestSuiteNoDB.java
@@ -23,6 +23,8 @@ import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
 import org.mockito.Mockito;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 
@@ -72,6 +74,8 @@ import com.google.common.collect.ImmutableList;
 
 public abstract class AnalyticsTestSuiteNoDB {
 
+    protected final Logger logger = LoggerFactory.getLogger(AnalyticsTestSuiteNoDB.class);
+
     protected final Long accountRecordId = 1L;
     protected final Long subscriptionEventRecordId = 2L;
     protected final Long invoiceRecordId = 3L;
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/TestBusinessInvoiceDao.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/TestBusinessInvoiceDao.java
index 5a50590..7258265 100644
--- a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/TestBusinessInvoiceDao.java
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/TestBusinessInvoiceDao.java
@@ -1,4 +1,165 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
 package com.ning.billing.osgi.bundles.analytics.dao;
 
-public class TestBusinessInvoiceDao {
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+import javax.sql.DataSource;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
+import com.ning.billing.osgi.bundles.analytics.AnalyticsTestSuiteNoDB;
+import com.ning.killbill.osgi.libs.killbill.OSGIKillbillDataSource;
+import com.ning.killbill.osgi.libs.killbill.OSGIKillbillLogService;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestBusinessInvoiceDao extends AnalyticsTestSuiteNoDB {
+
+    private final UUID accountId = UUID.randomUUID();
+    private final UUID invoiceId = UUID.randomUUID();
+    private final UUID bundleId = UUID.randomUUID();
+
+    private OSGIKillbillDataSource osgiKillbillDataSource;
+    private OSGIKillbillLogService osgiKillbillLogService;
+
+    @Override
+    @BeforeMethod
+    public void setUp() throws Exception {
+        super.setUp();
+
+        osgiKillbillDataSource = Mockito.mock(OSGIKillbillDataSource.class);
+
+        final DataSource dataSource = Mockito.mock(DataSource.class);
+        Mockito.when(osgiKillbillDataSource.getDataSource()).thenReturn(dataSource);
+
+        osgiKillbillLogService = Mockito.mock(OSGIKillbillLogService.class);
+        Mockito.doAnswer(new Answer() {
+            @Override
+            public Object answer(final InvocationOnMock invocation) throws Throwable {
+                logger.info(Arrays.toString(invocation.getArguments()));
+                return null;
+            }
+        }).when(osgiKillbillLogService).log(Mockito.anyInt(), Mockito.anyString());
+    }
+
+    @Test(groups = "fast")
+    public void testSanitization() throws Exception {
+        // One invoice, with two repairs and an external charge
+        final UUID subscriptionId1 = UUID.randomUUID();
+        final LocalDate startDate1 = new LocalDate(2013, 4, 1);
+        final LocalDate endDate1 = new LocalDate(2013, 4, 30);
+        final BigDecimal amount1 = new BigDecimal("30");
+        final InvoiceItem recurring1 = createInvoiceItem(InvoiceItemType.RECURRING, subscriptionId1, startDate1, endDate1, amount1, null);
+        final InvoiceItem repair1 = createInvoiceItem(InvoiceItemType.REPAIR_ADJ, subscriptionId1, startDate1, endDate1, amount1.negate(), recurring1.getId());
+        final LocalDate reparationEndDate1 = new LocalDate(2013, 4, 10);
+        final BigDecimal reparationAmount1 = new BigDecimal("10");
+        final InvoiceItem reparation1 = createInvoiceItem(InvoiceItemType.RECURRING, subscriptionId1, startDate1, reparationEndDate1, reparationAmount1, null);
+
+        final UUID subscriptionId2 = UUID.randomUUID();
+        final LocalDate startDate2 = new LocalDate(2013, 4, 10);
+        final LocalDate endDate2 = new LocalDate(2013, 4, 30);
+        final BigDecimal amount2 = new BigDecimal("20");
+        final InvoiceItem recurring2 = createInvoiceItem(InvoiceItemType.RECURRING, subscriptionId2, startDate2, endDate2, amount2, null);
+        final InvoiceItem repair2 = createInvoiceItem(InvoiceItemType.REPAIR_ADJ, subscriptionId2, startDate2, endDate2, amount2.negate(), recurring2.getId());
+        final LocalDate reparationEndDate2 = new LocalDate(2013, 4, 15);
+        final BigDecimal reparationAmount2 = new BigDecimal("5");
+        final InvoiceItem reparation2 = createInvoiceItem(InvoiceItemType.RECURRING, subscriptionId2, startDate2, reparationEndDate2, reparationAmount2, null);
+
+        final UUID externalChargeSubscriptionId = UUID.randomUUID();
+        final LocalDate externalStartDate = new LocalDate(2012, 1, 1);
+        final BigDecimal externalChargeAmount = BigDecimal.TEN;
+        final InvoiceItem externalCharge = createInvoiceItem(InvoiceItemType.EXTERNAL_CHARGE, externalChargeSubscriptionId, externalStartDate, null, externalChargeAmount, null);
+
+        final BusinessInvoiceDao invoiceDao = new BusinessInvoiceDao(osgiKillbillLogService, null, osgiKillbillDataSource, null);
+        final Collection<InvoiceItem> sanitizedInvoiceItems = invoiceDao.sanitizeInvoiceItems(ImmutableList.<InvoiceItem>of(recurring1, repair1, reparation1, recurring2, repair2, reparation2, externalCharge));
+        Assert.assertEquals(sanitizedInvoiceItems.size(), 2 + 2 + 1);
+        for (final InvoiceItem invoiceItem : sanitizedInvoiceItems) {
+            if (invoiceItem.getId().equals(recurring1.getId())) {
+                Assert.assertEquals(invoiceItem, recurring1);
+            } else if (invoiceItem.getId().equals(repair1.getId())) {
+                Assert.fail("Repair item 1 shouldn't be in the sanitized elements");
+            } else if (invoiceItem.getId().equals(reparation1.getId())) {
+                Assert.fail("Reparation item 1 shouldn't be in the sanitized elements");
+            } else if (invoiceItem.getId().equals(recurring2.getId())) {
+                Assert.assertEquals(invoiceItem, recurring2);
+            } else if (invoiceItem.getId().equals(repair2.getId())) {
+                Assert.fail("Repair item 2 shouldn't be in the sanitized elements");
+            } else if (invoiceItem.getId().equals(reparation2.getId())) {
+                Assert.fail("Reparation item 2 shouldn't be in the sanitized elements");
+            } else if (invoiceItem.getId().equals(externalCharge.getId())) {
+                Assert.assertEquals(invoiceItem, externalCharge);
+            } else {
+                if (InvoiceItemType.ITEM_ADJ.equals(invoiceItem.getInvoiceItemType())) {
+                    if (invoiceItem.getLinkedItemId().equals(recurring1.getId())) {
+                        Assert.assertEquals(invoiceItem.getAmount(), new BigDecimal("20").negate());
+                    } else if (invoiceItem.getLinkedItemId().equals(recurring2.getId())) {
+                        Assert.assertEquals(invoiceItem.getAmount(), new BigDecimal("15").negate());
+                    } else {
+                        Assert.fail("Shouldn't be in the sanitized elements: " + invoiceItem);
+                    }
+                } else {
+                    Assert.fail("Shouldn't be in the sanitized elements: " + invoiceItem);
+                }
+            }
+        }
+    }
+
+    private InvoiceItem createInvoiceItem(final InvoiceItemType invoiceItemType,
+                                          final UUID subscriptionId,
+                                          final LocalDate startDate,
+                                          final LocalDate endDate,
+                                          final BigDecimal amount,
+                                          @Nullable final UUID linkedItemId) {
+        final UUID invoiceItemId = UUID.randomUUID();
+
+        final InvoiceItem invoiceItem = Mockito.mock(InvoiceItem.class);
+        Mockito.when(invoiceItem.getId()).thenReturn(invoiceItemId);
+        Mockito.when(invoiceItem.getInvoiceItemType()).thenReturn(invoiceItemType);
+        Mockito.when(invoiceItem.getInvoiceId()).thenReturn(invoiceId);
+        Mockito.when(invoiceItem.getAccountId()).thenReturn(accountId);
+        Mockito.when(invoiceItem.getStartDate()).thenReturn(startDate);
+        Mockito.when(invoiceItem.getEndDate()).thenReturn(endDate);
+        Mockito.when(invoiceItem.getAmount()).thenReturn(amount);
+        Mockito.when(invoiceItem.getCurrency()).thenReturn(Currency.EUR);
+        Mockito.when(invoiceItem.getDescription()).thenReturn(UUID.randomUUID().toString());
+        Mockito.when(invoiceItem.getBundleId()).thenReturn(bundleId);
+        Mockito.when(invoiceItem.getSubscriptionId()).thenReturn(subscriptionId);
+        Mockito.when(invoiceItem.getPlanName()).thenReturn(UUID.randomUUID().toString());
+        Mockito.when(invoiceItem.getPhaseName()).thenReturn(UUID.randomUUID().toString());
+        Mockito.when(invoiceItem.getRate()).thenReturn(new BigDecimal("1203"));
+        Mockito.when(invoiceItem.getLinkedItemId()).thenReturn(linkedItemId);
+        Mockito.when(invoiceItem.getCreatedDate()).thenReturn(new DateTime(2016, 1, 22, 10, 56, 51, DateTimeZone.UTC));
+
+        return invoiceItem;
+    }
 }