killbill-memoizeit

Details

diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/AuditedInvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/AuditedInvoiceDao.java
index 08db235..3bb88f0 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/AuditedInvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/AuditedInvoiceDao.java
@@ -88,10 +88,10 @@ public class AuditedInvoiceDao implements InvoiceDao {
 
     @Inject
     public AuditedInvoiceDao(final IDBI dbi,
-                             final NextBillingDatePoster nextBillingDatePoster,
-                             final TagUserApi tagUserApi,
-                             final Clock clock,
-                             final Bus eventBus) {
+            final NextBillingDatePoster nextBillingDatePoster,
+            final TagUserApi tagUserApi,
+            final Clock clock,
+            final Bus eventBus) {
         this.invoiceSqlDao = dbi.onDemand(InvoiceSqlDao.class);
         this.invoicePaymentSqlDao = dbi.onDemand(InvoicePaymentSqlDao.class);
         this.invoiceItemSqlDao = dbi.onDemand(InvoiceItemSqlDao.class);
@@ -355,9 +355,12 @@ public class AuditedInvoiceDao implements InvoiceDao {
 
     @Override
     public InvoicePayment createRefund(final UUID paymentId, final BigDecimal requestedRefundAmount, final boolean isInvoiceAdjusted,
-                                       final Map<UUID, BigDecimal> invoiceItemIdsWithNullAmounts, final UUID paymentCookieId,
-                                       final CallContext context)
-            throws InvoiceApiException {
+            final Map<UUID, BigDecimal> invoiceItemIdsWithNullAmounts, final UUID paymentCookieId,
+            final CallContext context)
+                    throws InvoiceApiException {
+
+        final boolean isInvoiceItemAdjusted = isInvoiceAdjusted && invoiceItemIdsWithNullAmounts.size() > 0;
+
         return invoicePaymentSqlDao.inTransaction(new Transaction<InvoicePayment, InvoicePaymentSqlDao>() {
             @Override
             public InvoicePayment inTransaction(final InvoicePaymentSqlDao transactional, final TransactionStatus status) throws Exception {
@@ -371,8 +374,8 @@ public class AuditedInvoiceDao implements InvoiceDao {
 
                 // Retrieve the amounts to adjust, if needed
                 final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts = computeItemAdjustments(payment.getInvoiceId().toString(),
-                                                                                               transInvoiceDao,
-                                                                                               invoiceItemIdsWithNullAmounts);
+                        transInvoiceDao,
+                        invoiceItemIdsWithNullAmounts);
 
                 // Compute the actual amount to refund
                 final BigDecimal requestedPositiveAmount = computePositiveRefundAmount(payment, requestedRefundAmount, invoiceItemIdsWithAmounts);
@@ -385,8 +388,8 @@ public class AuditedInvoiceDao implements InvoiceDao {
                 }
 
                 final InvoicePayment refund = new DefaultInvoicePayment(UUID.randomUUID(), InvoicePaymentType.REFUND, paymentId,
-                                                                        payment.getInvoiceId(), context.getCreatedDate(), requestedPositiveAmount.negate(),
-                                                                        payment.getCurrency(), paymentCookieId, payment.getId());
+                        payment.getInvoiceId(), context.getCreatedDate(), requestedPositiveAmount.negate(),
+                        payment.getCurrency(), paymentCookieId, payment.getId());
                 transactional.create(refund, context);
 
                 // Retrieve invoice after the Refund
@@ -404,7 +407,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
                 // At this point, we created the refund which made the invoice balance positive and applied any existing
                 // available CBA to that invoice.
                 // We now need to adjust the invoice and/or invoice items if needed and specified.
-                if (isInvoiceAdjusted && invoiceItemIdsWithAmounts.size() == 0) {
+                if (isInvoiceAdjusted && !isInvoiceItemAdjusted) {
                     // Invoice adjustment
                     final BigDecimal maxBalanceToAdjust = (invoiceBalanceAfterRefund.compareTo(BigDecimal.ZERO) <= 0) ? BigDecimal.ZERO : invoiceBalanceAfterRefund;
                     final BigDecimal requestedPositiveAmountToAdjust = requestedPositiveAmount.compareTo(maxBalanceToAdjust) > 0 ? maxBalanceToAdjust : requestedPositiveAmount;
@@ -417,7 +420,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
                     for (final UUID invoiceItemId : invoiceItemIdsWithAmounts.keySet()) {
                         final BigDecimal adjAmount = invoiceItemIdsWithAmounts.get(invoiceItemId);
                         final InvoiceItem item = createAdjustmentItem(transInvoiceDao, invoice.getId(), invoiceItemId, adjAmount,
-                                                                      invoice.getCurrency(), context.getCreatedDate().toLocalDate());
+                                invoice.getCurrency(), context.getCreatedDate().toLocalDate());
                         transInvoiceItemDao.create(item, context);
                     }
                 }
@@ -443,7 +446,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
      * @throws InvoiceApiException
      */
     private Map<UUID, BigDecimal> computeItemAdjustments(final String invoiceId, final InvoiceSqlDao transInvoiceDao,
-                                                         final Map<UUID, BigDecimal> invoiceItemIdsWithNullAmounts) throws InvoiceApiException {
+            final Map<UUID, BigDecimal> invoiceItemIdsWithNullAmounts) throws InvoiceApiException {
         // Populate the missing amounts for individual items, if needed
         final Builder<UUID, BigDecimal> invoiceItemIdsWithAmountsBuilder = new Builder<UUID, BigDecimal>();
         if (invoiceItemIdsWithNullAmounts.size() == 0) {
@@ -460,13 +463,41 @@ public class AuditedInvoiceDao implements InvoiceDao {
 
         for (final UUID invoiceItemId : invoiceItemIdsWithNullAmounts.keySet()) {
             final BigDecimal adjAmount = Objects.firstNonNull(invoiceItemIdsWithNullAmounts.get(invoiceItemId),
-                                                              getInvoiceItemAmountForId(invoice, invoiceItemId));
-            invoiceItemIdsWithAmountsBuilder.put(invoiceItemId, adjAmount);
+                    getInvoiceItemAmountForId(invoice, invoiceItemId));
+            final BigDecimal adjAmountRemainingAfterRepair = computeItemAdjustmentAmount(invoiceItemId, adjAmount, invoice.getInvoiceItems());
+            if (adjAmountRemainingAfterRepair.compareTo(BigDecimal.ZERO) > 0) {
+                invoiceItemIdsWithAmountsBuilder.put(invoiceItemId,  adjAmountRemainingAfterRepair);
+            }
         }
 
         return invoiceItemIdsWithAmountsBuilder.build();
     }
 
+    /**
+     *
+     * @param invoiceItem                          item we are adjusting
+     * @param requestedPositiveAmountToAdjust      amount we are adjusting for that item
+     * @param invoiceItems                         list of all invoice items on this invoice
+     * @return                                     the amount we should really adjust based on whether or not the item got repaired
+     */
+    private BigDecimal computeItemAdjustmentAmount(final UUID invoiceItem, final BigDecimal requestedPositiveAmountToAdjust, final List<InvoiceItem> invoiceItems) {
+
+        BigDecimal positiveRepairedAmount = BigDecimal.ZERO;
+
+        final Collection<InvoiceItem> repairedItems = Collections2.filter(invoiceItems, new Predicate<InvoiceItem>() {
+            @Override
+            public boolean apply(@Nullable InvoiceItem input) {
+                return (input.getInvoiceItemType() == InvoiceItemType.REPAIR_ADJ && input.getLinkedItemId().equals(invoiceItem));
+            }
+        });
+        for (InvoiceItem cur : repairedItems) {
+            // Repair item are negative so we negate to make it positive
+            positiveRepairedAmount = positiveRepairedAmount.add(cur.getAmount().negate());
+        }
+        return (positiveRepairedAmount.compareTo(requestedPositiveAmountToAdjust) >= 0) ? BigDecimal.ZERO : requestedPositiveAmountToAdjust.subtract(positiveRepairedAmount);
+    }
+
+
     private BigDecimal getInvoiceItemAmountForId(final Invoice invoice, final UUID invoiceItemId) throws InvoiceApiException {
         for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
             if (invoiceItem.getId().equals(invoiceItemId)) {
@@ -520,7 +551,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
                     throw new InvoiceApiException(ErrorCode.INVOICE_PAYMENT_NOT_FOUND, invoicePaymentId.toString());
                 } else {
                     final InvoicePayment chargeBack = new DefaultInvoicePayment(UUID.randomUUID(), InvoicePaymentType.CHARGED_BACK, payment.getPaymentId(),
-                                                                                payment.getInvoiceId(), context.getCreatedDate(), requestedChargedBackAmout.negate(), payment.getCurrency(), null, payment.getId());
+                            payment.getInvoiceId(), context.getCreatedDate(), requestedChargedBackAmout.negate(), payment.getCurrency(), null, payment.getId());
                     transactional.create(chargeBack, context);
 
                     // Add audit
@@ -580,8 +611,8 @@ public class AuditedInvoiceDao implements InvoiceDao {
 
     @Override
     public InvoiceItem insertExternalCharge(final UUID accountId, @Nullable final UUID invoiceId, @Nullable final UUID bundleId, final String description,
-                                            final BigDecimal amount, final LocalDate effectiveDate, final Currency currency, final CallContext context)
-            throws InvoiceApiException {
+            final BigDecimal amount, final LocalDate effectiveDate, final Currency currency, final CallContext context)
+                    throws InvoiceApiException {
         return invoiceSqlDao.inTransaction(new Transaction<InvoiceItem, InvoiceSqlDao>() {
             @Override
             public InvoiceItem inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
@@ -594,8 +625,8 @@ public class AuditedInvoiceDao implements InvoiceDao {
                 }
 
                 final InvoiceItem externalCharge = new ExternalChargeInvoiceItem(invoiceIdForExternalCharge, accountId,
-                                                                                 bundleId, description,
-                                                                                 effectiveDate, amount, currency);
+                        bundleId, description,
+                        effectiveDate, amount, currency);
 
                 final InvoiceItemSqlDao transInvoiceItemDao = transactional.become(InvoiceItemSqlDao.class);
                 transInvoiceItemDao.create(externalCharge, context);
@@ -628,7 +659,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
 
     @Override
     public InvoiceItem insertCredit(final UUID accountId, @Nullable final UUID invoiceId, final BigDecimal positiveCreditAmount,
-                                    final LocalDate effectiveDate, final Currency currency, final CallContext context) {
+            final LocalDate effectiveDate, final Currency currency, final CallContext context) {
         return invoiceSqlDao.inTransaction(new Transaction<InvoiceItem, InvoiceSqlDao>() {
             @Override
             public InvoiceItem inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
@@ -658,13 +689,13 @@ public class AuditedInvoiceDao implements InvoiceDao {
 
     @Override
     public InvoiceItem insertInvoiceItemAdjustment(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId,
-                                                   final LocalDate effectiveDate, @Nullable final BigDecimal positiveAdjAmount,
-                                                   @Nullable final Currency currency, final CallContext context) {
+            final LocalDate effectiveDate, @Nullable final BigDecimal positiveAdjAmount,
+            @Nullable final Currency currency, final CallContext context) {
         return invoiceSqlDao.inTransaction(new Transaction<InvoiceItem, InvoiceSqlDao>() {
             @Override
             public InvoiceItem inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
                 final InvoiceItem invoiceItemAdjustment = createAdjustmentItem(transactional, invoiceId, invoiceItemId, positiveAdjAmount,
-                                                                               currency, effectiveDate);
+                        currency, effectiveDate);
                 insertItemAndAddCBAIfNeeded(transactional, invoiceItemAdjustment, context);
 
                 notifyBusOfInvoiceAdjustment(transactional, invoiceId, accountId, context.getUserToken());
@@ -694,7 +725,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
 
                 // First, adjust the same invoice with the CBA amount to "delete"
                 final InvoiceItem cbaAdjItem = new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(),
-                                                                               cbaItem.getId(), cbaItem.getAmount().negate(), cbaItem.getCurrency());
+                        cbaItem.getId(), cbaItem.getAmount().negate(), cbaItem.getCurrency());
                 invoiceItemSqlDao.create(cbaAdjItem, context);
 
                 // If there is more account credit than CBA we adjusted, we're done.
@@ -705,7 +736,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
                         throw new IllegalStateException("The account balance can't be lower than the amount adjusted");
                     }
                     final List<Invoice> invoicesFollowing = transactional.getInvoicesByAccountAfterDate(accountId.toString(),
-                                                                                                        invoice.getInvoiceDate().toDateTimeAtStartOfDay().toDate());
+                            invoice.getInvoiceDate().toDateTimeAtStartOfDay().toDate());
                     populateChildren(invoicesFollowing, transactional);
 
                     // The remaining amount to adjust (i.e. the amount of credits used on following invoices)
@@ -723,7 +754,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
                         for (final InvoiceItem cbaUsed : Lists.reverse(invoiceFollowing.getInvoiceItems())) {
                             // Ignore non CBA items or credits (CBA >= 0)
                             if (!InvoiceItemType.CBA_ADJ.equals(cbaUsed.getInvoiceItemType()) ||
-                                cbaUsed.getAmount().compareTo(BigDecimal.ZERO) >= 0) {
+                                    cbaUsed.getAmount().compareTo(BigDecimal.ZERO) >= 0) {
                                 continue;
                             }
 
@@ -745,7 +776,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
 
                         // Add the adjustment on that invoice
                         final InvoiceItem nextCBAAdjItem = new CreditBalanceAdjInvoiceItem(invoiceFollowing.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(),
-                                                                                           cbaItem.getId(), positiveCBAAdjItemAmount, cbaItem.getCurrency());
+                                cbaItem.getId(), positiveCBAAdjItemAmount, cbaItem.getCurrency());
                         invoiceItemSqlDao.create(nextCBAAdjItem, context);
                         if (positiveRemainderToAdjust.compareTo(BigDecimal.ZERO) == 0) {
                             break;
@@ -774,7 +805,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
      * @return the adjustment item
      */
     private InvoiceItem createAdjustmentItem(final InvoiceSqlDao transactional, final UUID invoiceId, final UUID invoiceItemId,
-                                             final BigDecimal positiveAdjAmount, final Currency currency, final LocalDate effectiveDate) throws InvoiceApiException {
+            final BigDecimal positiveAdjAmount, final Currency currency, final LocalDate effectiveDate) throws InvoiceApiException {
         // First, retrieve the invoice item in question
         final InvoiceItemSqlDao invoiceItemSqlDao = transactional.become(InvoiceItemSqlDao.class);
         final InvoiceItem invoiceItemToBeAdjusted = invoiceItemSqlDao.getById(invoiceItemId.toString());
@@ -832,7 +863,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
         if (invoice.getBalance().compareTo(BigDecimal.ZERO) < 0) {
             final InvoiceItemSqlDao transInvoiceItemDao = transactional.become(InvoiceItemSqlDao.class);
             final InvoiceItem cbaAdjItem = new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(),
-                                                                           invoice.getBalance().negate(), invoice.getCurrency());
+                    invoice.getBalance().negate(), invoice.getCurrency());
             transInvoiceItemDao.create(cbaAdjItem, context);
 
         }
@@ -902,8 +933,8 @@ public class AuditedInvoiceDao implements InvoiceDao {
             if (item.getInvoiceItemType() == InvoiceItemType.RECURRING) {
                 final RecurringInvoiceItem recurringInvoiceItem = (RecurringInvoiceItem) item;
                 if ((recurringInvoiceItem.getEndDate() != null) &&
-                    (recurringInvoiceItem.getAmount() == null ||
-                     recurringInvoiceItem.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
+                        (recurringInvoiceItem.getAmount() == null ||
+                        recurringInvoiceItem.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
                     subscriptionForNextNotification = recurringInvoiceItem.getSubscriptionId();
                     shouldBeNotified = true;
                     break;
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
index 54fce43..2d3b95b 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
@@ -570,6 +570,77 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
     }
 
     @Test(groups = "slow")
+    private void testFullRefundWithRepairAndInvoiceItemAdjustment() throws InvoiceApiException {
+        final BigDecimal refundAmount = new BigDecimal("20.00");
+        testRefundWithRepairAndInvoiceItemAdjustmentInternal(refundAmount);
+    }
+
+    @Test(groups = "slow")
+    private void testPartialRefundWithRepairAndInvoiceItemAdjustment() throws InvoiceApiException {
+        final BigDecimal refundAmount = new BigDecimal("7.00");
+        testRefundWithRepairAndInvoiceItemAdjustmentInternal(refundAmount);
+    }
+
+    private void testRefundWithRepairAndInvoiceItemAdjustmentInternal(final BigDecimal refundAmount) throws InvoiceApiException {
+
+        final UUID accountId = UUID.randomUUID();
+        final UUID bundleId = UUID.randomUUID();
+        final LocalDate targetDate1 = new LocalDate(2011, 10, 6);
+
+        final Invoice invoice = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD);
+        invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), true, context);
+
+        final LocalDate startDate = new LocalDate(2011, 3, 1);
+        final LocalDate endDate = startDate.plusMonths(1);
+
+        final BigDecimal amount = new BigDecimal("20.0");
+        ;
+
+        // Recurring item
+        final RecurringInvoiceItem item2 = new RecurringInvoiceItem(invoice.getId(), accountId, bundleId, UUID.randomUUID(), "test plan", "test phase B", startDate,
+                endDate, amount, amount, Currency.USD);
+        invoiceItemSqlDao.create(item2, context);
+        BigDecimal balance = invoiceDao.getAccountBalance(accountId);
+        assertEquals(balance.compareTo(new BigDecimal("20.00")), 0);
+
+        // Pay the whole thing
+        final UUID paymentId = UUID.randomUUID();
+        final BigDecimal payment1 = amount;
+        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), new DateTime(), payment1, Currency.USD);
+        invoicePaymentDao.create(payment, context);
+        balance = invoiceDao.getAccountBalance(accountId);
+        assertEquals(balance.compareTo(new BigDecimal("0.00")), 0);
+
+
+        // Repair the item (And add CBA item that should be generated)
+        final InvoiceItem repairItem = new RepairAdjInvoiceItem(invoice.getId(), accountId, startDate, endDate, amount.negate(), Currency.USD, item2.getId());
+        invoiceItemSqlDao.create(repairItem, context);
+
+        final InvoiceItem cbaItem = new CreditBalanceAdjInvoiceItem(invoice.getId(), accountId, startDate, amount, Currency.USD);
+        invoiceItemSqlDao.create(cbaItem, context);
+
+        Map<UUID, BigDecimal> itemAdjustment = new HashMap<UUID, BigDecimal>();
+        itemAdjustment.put(item2.getId(), refundAmount);
+
+        invoiceDao.createRefund(paymentId, refundAmount, true, itemAdjustment, UUID.randomUUID(), context);
+        balance = invoiceDao.getAccountBalance(accountId);
+
+        final boolean partialRefund = refundAmount.compareTo(amount) < 0;
+        final BigDecimal cba = invoiceDao.getAccountCBA(accountId);
+        final Invoice savedInvoice = invoiceDao.getById(invoice.getId());
+        assertEquals(cba.compareTo(new BigDecimal("20.0")), 0);
+        if (partialRefund) {
+            // IB = 20 (rec) - 20 (repair) + 20 (cba) - (20 -7) = 7;  AB = IB - CBA = 7 - 20 = -13
+            assertEquals(balance.compareTo(new BigDecimal("-13.0")), 0);
+            assertEquals(savedInvoice.getInvoiceItems().size(), 3);
+        } else {
+            assertEquals(balance.compareTo(new BigDecimal("0.0")), 0);
+            assertEquals(savedInvoice.getInvoiceItems().size(), 3);
+        }
+    }
+
+
+    @Test(groups = "slow")
     public void testAccountBalanceWithSmallRefundAndCBANoAdj() throws InvoiceApiException {
         BigDecimal refundAmount = new BigDecimal("7.00");
         BigDecimal expectedBalance = new BigDecimal("-3.00");