killbill-memoizeit

junction: compute account BCD from ACCOUNT alignments only Signed-off-by:

12/3/2018 7:06:34 PM

Details

diff --git a/api/src/main/java/org/killbill/billing/junction/BillingEvent.java b/api/src/main/java/org/killbill/billing/junction/BillingEvent.java
index 7af38bf..9588349 100644
--- a/api/src/main/java/org/killbill/billing/junction/BillingEvent.java
+++ b/api/src/main/java/org/killbill/billing/junction/BillingEvent.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -22,6 +22,7 @@ import java.math.BigDecimal;
 import java.util.List;
 
 import org.joda.time.DateTime;
+import org.killbill.billing.catalog.api.BillingAlignment;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.Currency;
@@ -41,6 +42,11 @@ public interface BillingEvent extends Comparable<BillingEvent> {
     int getBillCycleDayLocal();
 
     /**
+     * @return the BillingAlignment for this transition
+     */
+    BillingAlignment getBillingAlignment();
+
+    /**
      * @return the subscription
      */
     SubscriptionBase getSubscription();
@@ -105,4 +111,4 @@ public interface BillingEvent extends Comparable<BillingEvent> {
      * @return the catalog version (effective date) associated with this billing event.
      */
     public DateTime getCatalogEffectiveDate();
-}
\ No newline at end of file
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
index 2892e26..e61343d 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -43,6 +43,7 @@ import org.killbill.billing.callcontext.MutableInternalCallContext;
 import org.killbill.billing.catalog.MockPlan;
 import org.killbill.billing.catalog.MockPlanPhase;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingAlignment;
 import org.killbill.billing.catalog.api.BillingMode;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Currency;
@@ -362,6 +363,11 @@ public class TestInvoiceHelper {
             }
 
             @Override
+            public BillingAlignment getBillingAlignment() {
+                return null;
+            }
+
+            @Override
             public SubscriptionBase getSubscription() {
                 return subscription;
             }
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
index ebc9f21..43da1ab 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -24,6 +24,7 @@ import java.util.List;
 import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
+import org.killbill.billing.catalog.api.BillingAlignment;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Catalog;
 import org.killbill.billing.catalog.api.CatalogApiException;
@@ -42,6 +43,7 @@ import com.google.common.collect.Lists;
 public class DefaultBillingEvent implements BillingEvent {
 
     private final int billCycleDayLocal;
+    private final BillingAlignment billingAlignment;
     private final SubscriptionBase subscription;
     private final DateTime effectiveDate;
     private final PlanPhase planPhase;
@@ -64,6 +66,7 @@ public class DefaultBillingEvent implements BillingEvent {
     public DefaultBillingEvent(final SubscriptionInternalEvent transition,
                                final SubscriptionBase subscription,
                                final int billCycleDayLocal,
+                               final BillingAlignment billingAlignment,
                                final Currency currency,
                                final Catalog catalog) throws CatalogApiException {
         final boolean isActive = transition.getTransitionType() != SubscriptionBaseTransitionType.CANCEL;
@@ -93,6 +96,7 @@ public class DefaultBillingEvent implements BillingEvent {
         this.catalogEffectiveDate = plan == null ? null : new DateTime(plan.getCatalog().getEffectiveDate());
 
         this.billCycleDayLocal = billCycleDayLocal;
+        this.billingAlignment = billingAlignment;
         this.catalog = catalog;
         this.currency = currency;
         this.description = transition.getTransitionType().toString();
@@ -128,6 +132,7 @@ public class DefaultBillingEvent implements BillingEvent {
         this.isDisableEvent = isDisableEvent;
         this.nextPlanPhase = isDisableEvent ? null : planPhase;
         this.catalogEffectiveDate = plan != null ? new DateTime(plan.getCatalog().getEffectiveDate()) : null;
+        this.billingAlignment = null;
     }
 
     @Override
@@ -182,6 +187,11 @@ public class DefaultBillingEvent implements BillingEvent {
     }
 
     @Override
+    public BillingAlignment getBillingAlignment() {
+        return billingAlignment;
+    }
+
+    @Override
     public SubscriptionBase getSubscription() {
         return subscription;
     }
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
index 42a2890..bbe7531 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
@@ -185,9 +185,13 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
 
         // If dryRun is specified, we don't want to to update the account BCD value, so we initialize the flag updatedAccountBCD to true
         if (currentAccountBCD == 0 && !dryRunMode) {
-            BillingEvent oldestBillingEvent = null;
+            BillingEvent oldestAccountAlignedBillingEvent = null;
 
             for (final BillingEvent event : result) {
+                if (event.getBillingAlignment() != BillingAlignment.ACCOUNT) {
+                    continue;
+                }
+
                 final BigDecimal recurringPrice = event.getRecurringPrice(event.getEffectiveDate());
                 final boolean hasRecurringPrice = recurringPrice != null; // Note: could be zero (BCD would still be set, by convention)
                 final boolean hasUsage = event.getUsages() != null && !event.getUsages().isEmpty();
@@ -197,19 +201,19 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
                     continue;
                 }
 
-                if (oldestBillingEvent == null ||
-                    event.getEffectiveDate().compareTo(oldestBillingEvent.getEffectiveDate()) < 0 ||
-                    (event.getEffectiveDate().compareTo(oldestBillingEvent.getEffectiveDate()) == 0 && event.getTotalOrdering().compareTo(oldestBillingEvent.getTotalOrdering()) < 0)) {
-                    oldestBillingEvent = event;
+                if (oldestAccountAlignedBillingEvent == null ||
+                    event.getEffectiveDate().compareTo(oldestAccountAlignedBillingEvent.getEffectiveDate()) < 0 ||
+                    (event.getEffectiveDate().compareTo(oldestAccountAlignedBillingEvent.getEffectiveDate()) == 0 && event.getTotalOrdering().compareTo(oldestAccountAlignedBillingEvent.getTotalOrdering()) < 0)) {
+                    oldestAccountAlignedBillingEvent = event;
                 }
             }
 
-            if (oldestBillingEvent == null) {
+            if (oldestAccountAlignedBillingEvent == null) {
                 return;
             }
 
             // BCD in the account timezone
-            final int accountBCDCandidate = oldestBillingEvent.getBillCycleDayLocal();
+            final int accountBCDCandidate = oldestAccountAlignedBillingEvent.getBillCycleDayLocal();
             if (accountBCDCandidate != 0) {
                 log.info("Setting account BCD='{}', accountId='{}'", accountBCDCandidate, account.getId());
                 accountApi.updateBCD(account.getExternalKey(), accountBCDCandidate, context);
@@ -245,6 +249,8 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
 
             Integer overridenBCD = null;
             for (final EffectiveSubscriptionInternalEvent transition : billingTransitions) {
+                final BillingAlignment alignment = catalog.billingAlignment(getPlanPhaseSpecifierFromTransition(catalog, transition), transition.getEffectiveTransitionTime(), subscription.getStartDate());
+
                 //
                 // A BCD_CHANGE transition defines a new billCycleDayLocal for the subscription and this overrides whatever computation
                 // occurs below (which is based on billing alignment policy). Also multiple of those BCD_CHANGE transitions could occur,
@@ -253,17 +259,16 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
                 overridenBCD = transition.getNextBillCycleDayLocal() != null ? transition.getNextBillCycleDayLocal() : overridenBCD;
                 final int bcdLocal = overridenBCD != null ?
                                      overridenBCD :
-                                     calculateBcdForTransition(catalog, bcdCache, baseSubscription, subscription, currentAccountBCD, transition, context);
+                                     calculateBcdForTransition(alignment, bcdCache, baseSubscription, subscription, currentAccountBCD, context);
 
-                final BillingEvent event = new DefaultBillingEvent(transition, subscription, bcdLocal, account.getCurrency(), catalog);
+                final BillingEvent event = new DefaultBillingEvent(transition, subscription, bcdLocal, alignment, account.getCurrency(), catalog);
                 result.add(event);
             }
         }
     }
 
-    private int calculateBcdForTransition(final Catalog catalog, final Map<UUID, Integer> bcdCache, final SubscriptionBase baseSubscription, final SubscriptionBase subscription, final int accountBillCycleDayLocal, final EffectiveSubscriptionInternalEvent transition, final InternalTenantContext internalTenantContext)
-            throws CatalogApiException {
-        BillingAlignment alignment = catalog.billingAlignment(getPlanPhaseSpecifierFromTransition(catalog, transition), transition.getEffectiveTransitionTime(), subscription.getStartDate());
+    private int calculateBcdForTransition(final BillingAlignment realBillingAlignment, final Map<UUID, Integer> bcdCache, final SubscriptionBase baseSubscription, final SubscriptionBase subscription, final int accountBillCycleDayLocal, final InternalTenantContext internalTenantContext) {
+        BillingAlignment alignment = realBillingAlignment;
         if (alignment == BillingAlignment.ACCOUNT && accountBillCycleDayLocal == 0) {
             alignment = BillingAlignment.SUBSCRIPTION;
         }
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java
index 106b185..ae11ca8 100644
--- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java
@@ -209,6 +209,82 @@ public class TestDefaultInternalBillingApi extends JunctionTestSuiteWithEmbedded
         }
     }
 
+    @Test(groups = "slow")
+    public void testBCDUpdateMultipleSubscriptionsAccountAndSubscriptionAligned() throws Exception {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        // Account with no BCD
+        final Account account = createAccount(getAccountData(0));
+        Assert.assertEquals(account.getBillCycleDayLocal(), (Integer) 0);
+
+        // Create 2 BASE entitlements
+        final String bundleKey1 = UUID.randomUUID().toString();
+        final String bundleKey2 = UUID.randomUUID().toString();
+        final EntitlementSpecifier entitlementSpecifierBase1 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Pistol", BillingPeriod.ANNUAL, "gunclubDiscountNoTrial", null));
+        final EntitlementSpecifier entitlementSpecifierBase2 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null));
+        final BaseEntitlementWithAddOnsSpecifier specifier1 = new DefaultBaseEntitlementWithAddOnsSpecifier(null, bundleKey1, ImmutableList.of(entitlementSpecifierBase1), null, null, false);
+        final BaseEntitlementWithAddOnsSpecifier specifier2 = new DefaultBaseEntitlementWithAddOnsSpecifier(null, bundleKey2, ImmutableList.of(entitlementSpecifierBase2), null, null, false);
+        testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.CREATE, NextEvent.BLOCK);
+        entitlementApi.createBaseEntitlementsWithAddOns(account.getId(),
+                                                        ImmutableList.of(specifier1, specifier2),
+                                                        false,
+                                                        ImmutableList.<PluginProperty>of(),
+                                                        callContext);
+        assertListenerStatus();
+
+        final List<Entitlement> entitlements = entitlementApi.getAllEntitlementsForAccountId(account.getId(), callContext);
+        Assert.assertEquals(entitlements.size(), 2);
+        for (final Entitlement entitlement : entitlements) {
+            if ("pistol-annual-gunclub-discount-notrial".equals(entitlement.getLastActivePlan().getName())) {
+                // SUBSCRIPTION aligned
+                Assert.assertEquals(entitlement.getBillCycleDayLocal(), (Integer) 7);
+            } else {
+                // ACCOUNT aligned
+                Assert.assertNull(entitlement.getBillCycleDayLocal());
+            }
+        }
+
+        // Account still has no BCD
+        final Account accountNoBCD = accountApi.getAccountById(account.getId(), callContext);
+        Assert.assertEquals(accountNoBCD.getBillCycleDayLocal(), (Integer) 0);
+
+        List<BillingEvent> events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext));
+        Assert.assertEquals(events.size(), 4);
+        for (final BillingEvent billingEvent : events) {
+            if ("pistol-annual-gunclub-discount-notrial".equals(billingEvent.getPlan().getName())) {
+                Assert.assertEquals(billingEvent.getBillCycleDayLocal(), 7);
+            } else {
+                Assert.assertEquals(billingEvent.getBillCycleDayLocal(), 6);
+            }
+        }
+
+        // Verify BCD
+        final Account accountWithBCD = accountApi.getAccountById(account.getId(), callContext);
+        Assert.assertEquals(accountWithBCD.getBillCycleDayLocal(), (Integer) 6);
+
+        // Verify GET
+        final List<Entitlement> entitlementsUpdated = entitlementApi.getAllEntitlementsForAccountId(account.getId(), callContext);
+        Assert.assertEquals(entitlementsUpdated.size(), 2);
+        for (final Entitlement entitlement : entitlementsUpdated) {
+            if ("pistol-annual-gunclub-discount-notrial".equals(entitlement.getLastActivePlan().getName())) {
+                Assert.assertEquals(entitlement.getBillCycleDayLocal(), (Integer) 7);
+            } else {
+                Assert.assertEquals(entitlement.getBillCycleDayLocal(), (Integer) 6);
+            }
+        }
+
+        events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext));
+        Assert.assertEquals(events.size(), 4);
+        for (final BillingEvent billingEvent : events) {
+            if ("pistol-annual-gunclub-discount-notrial".equals(billingEvent.getPlan().getName())) {
+                Assert.assertEquals(billingEvent.getBillCycleDayLocal(), 7);
+            } else {
+                Assert.assertEquals(billingEvent.getBillCycleDayLocal(), 6);
+            }
+        }
+    }
+
     // This test was originally for https://github.com/killbill/killbill/issues/123.
     // The invocationCount > 0 was to trigger an issue where events would come out-of-order randomly.
     // While the bug shouldn't occur anymore, we're keeping it just in case (the test will also try to insert the events out-of-order manually).