killbill-memoizeit

payment: post an event on the bus if there is no default payment

8/29/2012 1:18:53 PM

Details

diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index 9109bbd..c42722b 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -97,6 +97,28 @@ public class TestOverdueIntegration extends TestIntegrationBase {
     private String productName;
     private BillingPeriod term;
 
+    final PaymentMethodPlugin paymentMethodPlugin = new PaymentMethodPlugin() {
+        @Override
+        public boolean isDefaultPaymentMethod() {
+            return false;
+        }
+
+        @Override
+        public String getValueString(final String key) {
+            return null;
+        }
+
+        @Override
+        public List<PaymentMethodKVInfo> getProperties() {
+            return null;
+        }
+
+        @Override
+        public String getExternalPaymentMethodId() {
+            return UUID.randomUUID().toString();
+        }
+    };
+
     @BeforeMethod(groups = "slow")
     public void setupOverdue() throws Exception {
         final String configXml = "<overdueConfig>" +
@@ -149,28 +171,7 @@ public class TestOverdueIntegration extends TestIntegrationBase {
         account = createAccountWithPaymentMethod(getAccountData(0));
         assertNotNull(account);
 
-        final PaymentMethodPlugin info = new PaymentMethodPlugin() {
-            @Override
-            public boolean isDefaultPaymentMethod() {
-                return false;
-            }
-
-            @Override
-            public String getValueString(final String key) {
-                return null;
-            }
-
-            @Override
-            public List<PaymentMethodKVInfo> getProperties() {
-                return null;
-            }
-
-            @Override
-            public String getExternalPaymentMethodId() {
-                return UUID.randomUUID().toString();
-            }
-        };
-        paymentApi.addPaymentMethod(BeatrixModule.PLUGIN_NAME, account, true, info, context);
+        paymentApi.addPaymentMethod(BeatrixModule.PLUGIN_NAME, account, true, paymentMethodPlugin, context);
 
         bundle = entitlementUserApi.createBundleForAccount(account.getId(), "whatever", context);
 
@@ -274,6 +275,102 @@ public class TestOverdueIntegration extends TestIntegrationBase {
         assertEquals(invoiceUserApi.getAccountBalance(account.getId()).compareTo(new BigDecimal("-8.88")), 0);
     }
 
+    @Test(groups = "slow")
+    public void testOverdueStateIfNoPaymentMethod() throws Exception {
+        // This test is similar to the previous one - but there is no default payment method on the account, so there
+        // won't be any payment retry
+
+        clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+        // Make sure the account doesn't have any payment method
+        accountUserApi.removePaymentMethod(account.getId(), context);
+
+        // Create subscription
+        final Subscription baseSubscription = createSubscriptionAndCheckForCompletion(bundle.getId(), productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
+
+        invoiceChecker.checkInvoice(account.getId(), 1, new ExpectedItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 5, 1));
+
+        // DAY 30 have to get out of trial before first payment. A payment error, one for each invoice, should be on the bus (because there is no payment method)
+        addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.PAYMENT_ERROR);
+
+        invoiceChecker.checkInvoice(account.getId(), 2, new ExpectedItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 6, 30));
+
+        // Should still be in clear state
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // DAY 45 - 15 days after invoice
+        addDaysAndCheckForCompletion(15);
+
+        // Should still be in clear state
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+
+        // DAY 65 - 35 days after invoice
+        // Single PAYMENT_ERROR here here triggered by the invoice
+        addDaysAndCheckForCompletion(20, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR);
+
+        invoiceChecker.checkInvoice(account.getId(), 3, new ExpectedItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 7, 31));
+
+        // Now we should be in OD1
+        checkODState("OD1");
+        checkChangePlanWithOverdueState(baseSubscription, true);
+
+        // DAY 67 - 37 days after invoice
+        addDaysAndCheckForCompletion(2);
+
+        // Should still be in OD1
+        checkODState("OD1");
+        checkChangePlanWithOverdueState(baseSubscription, true);
+
+        // DAY 75 - 45 days after invoice
+        addDaysAndCheckForCompletion(8);
+
+        // Should still be in OD1
+        checkODState("OD2");
+        checkChangePlanWithOverdueState(baseSubscription, true);
+
+        // DAY 85 - 55 days after invoice
+        addDaysAndCheckForCompletion(10);
+
+        // Should now be in OD2 state once the update is processed
+        checkODState("OD3");
+        checkChangePlanWithOverdueState(baseSubscription, true);
+
+        // Add a payment method and set it as default
+        paymentApi.addPaymentMethod(BeatrixModule.PLUGIN_NAME, account, true, paymentMethodPlugin, context);
+
+        // Pay all invoices
+        final Collection<Invoice> invoices = invoiceApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday());
+        for (final Invoice invoice : invoices) {
+            if (invoice.getBalance().compareTo(BigDecimal.ZERO) > 0) {
+                createPaymentAndCheckForCompletion(account, invoice, NextEvent.PAYMENT);
+            }
+        }
+
+        checkODState(BlockingApi.CLEAR_STATE_NAME);
+        checkChangePlanWithOverdueState(baseSubscription, false);
+
+        invoiceChecker.checkRepairedInvoice(account.getId(), 3,
+                                            new ExpectedItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+                                            // We paid up to 07-31, hence the adjustment
+                                            new ExpectedItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-249.95")),
+                                            new ExpectedItemCheck(new LocalDate(2012, 7, 25), new LocalDate(2012, 7, 25), InvoiceItemType.CBA_ADJ, new BigDecimal("249.95")));
+        invoiceChecker.checkInvoice(account.getId(), 4,
+                                    // Note the end date here is not 07-25, but 07-15. The overdue configuration disabled invoicing between 07-15 and 07-25 (e.g. the bundle
+                                    // was inaccessible, hence we didn't want to charge the customer for that period, even though the account was overdue).
+                                    new ExpectedItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 15), InvoiceItemType.RECURRING, new BigDecimal("124.98")),
+                                    // Item for the upgraded recurring plan
+                                    new ExpectedItemCheck(new LocalDate(2012, 7, 25), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("116.09")),
+                                    // Credits consumed
+                                    new ExpectedItemCheck(new LocalDate(2012, 7, 25), new LocalDate(2012, 7, 25), InvoiceItemType.CBA_ADJ, new BigDecimal("-241.07")));
+        invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 7, 31));
+
+        // Verify the account balance: 249.95 - 124.98 - 116.09
+        assertEquals(invoiceUserApi.getAccountBalance(account.getId()).compareTo(new BigDecimal("-8.88")), 0);
+    }
+
     private void checkChangePlanWithOverdueState(final Subscription subscription, final boolean shouldFail) {
         if (shouldFail) {
             try {
diff --git a/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
index ee41b6d..55b852f 100644
--- a/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
@@ -48,6 +48,7 @@ import com.ning.billing.payment.api.DefaultPaymentErrorEvent;
 import com.ning.billing.payment.api.DefaultPaymentInfoEvent;
 import com.ning.billing.payment.api.Payment;
 import com.ning.billing.payment.api.PaymentApiException;
+import com.ning.billing.payment.api.PaymentErrorEvent;
 import com.ning.billing.payment.api.PaymentStatus;
 import com.ning.billing.payment.dao.PaymentAttemptModelDao;
 import com.ning.billing.payment.dao.PaymentDao;
@@ -209,12 +210,22 @@ public class PaymentProcessor extends ProcessorBase {
         // Use the special external payment plugin to handle external payments
         final PaymentPluginApi plugin;
         final UUID paymentMethodId;
-        if (isExternalPayment) {
-            plugin = paymentMethodProcessor.getExternalPaymentProviderPlugin(account, context);
-            paymentMethodId = paymentMethodProcessor.getExternalPaymentMethod(account).getId();
-        } else {
-            plugin = getPaymentProviderPlugin(account);
-            paymentMethodId = account.getPaymentMethodId();
+        try {
+            if (isExternalPayment) {
+                plugin = paymentMethodProcessor.getExternalPaymentProviderPlugin(account, context);
+                paymentMethodId = paymentMethodProcessor.getExternalPaymentMethod(account).getId();
+            } else {
+                plugin = getPaymentProviderPlugin(account);
+                paymentMethodId = account.getPaymentMethodId();
+            }
+        } catch (PaymentApiException e) {
+            // This event will be caught by overdue to refresh the overdue state, if needed.
+            // Note that at this point, we don't know the exact invoice balance (see getAndValidatePaymentAmount() below).
+            // This means that events will be posted for null and zero dollar invoices (e.g. trials).
+            final PaymentErrorEvent event = new DefaultPaymentErrorEvent(account.getId(), invoiceId, null,
+                                                                         ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD.toString(), context.getUserToken());
+            postPaymentEvent(event, account.getId());
+            throw e;
         }
 
         try {