killbill-memoizeit

junction: revisit account BCD calculation Before, we were

11/30/2018 9:22:29 AM

Details

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 e033ad6..fd3b880 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
@@ -18,6 +18,7 @@
 
 package org.killbill.billing.junction.plumbing.billing;
 
+import java.math.BigDecimal;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -28,6 +29,7 @@ import java.util.UUID;
 
 import javax.annotation.Nullable;
 
+import org.joda.time.DateTime;
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.account.api.AccountApiException;
 import org.killbill.billing.account.api.AccountInternalApi;
@@ -139,9 +141,10 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
 
     private void addBillingEventsForBundles(final List<SubscriptionBaseBundle> bundles, final ImmutableAccountData account, final DryRunArguments dryRunArguments, final InternalCallContext context,
                                             final DefaultBillingEventSet result, final Set<UUID> skipSubscriptionsSet, final Catalog catalog, final List<Tag> tagsForAccount) throws AccountApiException, CatalogApiException, SubscriptionBaseApiException {
-
         final boolean dryRunMode = dryRunArguments != null;
 
+        final int currentAccountBCD = accountApi.getBCD(account.getId(), context);
+
         // In dryRun mode, when we care about invoice generated for new BASE subscription, no such bundle exists yet; we still
         // want to tap into subscriptionBase logic, so we make up a bundleId
         if (dryRunArguments != null &&
@@ -150,8 +153,7 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
             final UUID fakeBundleId = UUIDs.randomUUID();
             final List<SubscriptionBase> subscriptions = subscriptionApi.getSubscriptionsForBundle(fakeBundleId, dryRunArguments, context);
 
-            addBillingEventsForSubscription(account, subscriptions, null, dryRunMode, context, result, skipSubscriptionsSet, catalog);
-
+            addBillingEventsForSubscription(account, subscriptions, null, currentAccountBCD, context, result, skipSubscriptionsSet, catalog);
         }
 
         final Map<UUID, List<SubscriptionBase>> subscriptionsForAccount = subscriptionApi.getSubscriptionsForAccount(catalog, context);
@@ -160,11 +162,11 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
             final DryRunArguments dryRunArgumentsForBundle = (dryRunArguments != null &&
                                                               dryRunArguments.getBundleId() != null &&
                                                               dryRunArguments.getBundleId().equals(bundle.getId())) ?
-                                                              dryRunArguments : null;
+                                                             dryRunArguments : null;
             final List<SubscriptionBase> subscriptions;
             // In dryRun mode, optimization is intentionally left as is, since is not a common path.
             if (dryRunArgumentsForBundle == null || dryRunArgumentsForBundle.getAction() == null) {
-                subscriptions = getSubscriptionsForAccountByBundleId(subscriptionsForAccount,bundle.getId());
+                subscriptions = getSubscriptionsForAccountByBundleId(subscriptionsForAccount, bundle.getId());
             } else {
                 subscriptions = subscriptionApi.getSubscriptionsForBundle(bundle.getId(), dryRunArgumentsForBundle, context);
             }
@@ -178,7 +180,27 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
                 }
             } else { // billing is not off
                 final SubscriptionBase baseSubscription = subscriptions != null && !subscriptions.isEmpty() ? subscriptions.get(0) : null;
-                addBillingEventsForSubscription(account, subscriptions, baseSubscription, dryRunMode, context, result, skipSubscriptionsSet, catalog);
+                addBillingEventsForSubscription(account, subscriptions, baseSubscription, currentAccountBCD, context, result, skipSubscriptionsSet, catalog);
+            }
+        }
+
+        // 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) {
+            DateTime oldestNonZeroRecurringCharge = null;
+
+            for (final BillingEvent event : result) {
+                final BigDecimal recurringPrice = event.getRecurringPrice(event.getEffectiveDate());
+                final boolean nonZeroRecurringCharge = recurringPrice != null && BigDecimal.ZERO.compareTo(recurringPrice) < 0;
+                if (nonZeroRecurringCharge &&
+                    (oldestNonZeroRecurringCharge == null || event.getEffectiveDate().compareTo(oldestNonZeroRecurringCharge) < 0)) {
+                    oldestNonZeroRecurringCharge = event.getEffectiveDate();
+                }
+            }
+
+            final int accountBCDCandidate = oldestNonZeroRecurringCharge == null ? 0 : oldestNonZeroRecurringCharge.getDayOfMonth();
+            if (accountBCDCandidate != 0) {
+                log.info("Setting account BCD='{}', accountId='{}'", accountBCDCandidate, account.getId());
+                accountApi.updateBCD(account.getExternalKey(), accountBCDCandidate, context);
             }
         }
     }
@@ -186,19 +208,13 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
     private void addBillingEventsForSubscription(final ImmutableAccountData account,
                                                  final List<SubscriptionBase> subscriptions,
                                                  final SubscriptionBase baseSubscription,
-                                                 final boolean dryRunMode,
+                                                 final int currentAccountBCD,
                                                  final InternalCallContext context,
                                                  final DefaultBillingEventSet result,
                                                  final Set<UUID> skipSubscriptionsSet,
-                                                 final Catalog catalog) throws AccountApiException, CatalogApiException, SubscriptionBaseApiException {
-
-        // If dryRun is specified, we don't want to to update the account BCD value, so we initialize the flag updatedAccountBCD to true
-        boolean updatedAccountBCD = dryRunMode;
-
+                                                 final Catalog catalog) throws CatalogApiException {
         final Map<UUID, Integer> bcdCache = new HashMap<UUID, Integer>();
 
-        int currentAccountBCD = accountApi.getBCD(account.getId(), context);
-
         for (final SubscriptionBase subscription : subscriptions) {
 
             final List<EffectiveSubscriptionInternalEvent> billingTransitions = subscriptionApi.getBillingTransitions(subscription, context);
@@ -222,12 +238,6 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
                                      overridenBCD :
                                      calculateBcdForTransition(catalog, bcdCache, baseSubscription, subscription, currentAccountBCD, transition, context);
 
-                if (currentAccountBCD == 0 && !updatedAccountBCD) {
-                    log.info("Setting account BCD='{}', accountId='{}'", bcdLocal, account.getId());
-                    accountApi.updateBCD(account.getExternalKey(), bcdLocal, context);
-                    updatedAccountBCD = true;
-                }
-
                 final BillingEvent event = new DefaultBillingEvent(transition, subscription, bcdLocal, account.getCurrency(), catalog);
                 result.add(event);
             }
@@ -235,7 +245,7 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
     }
 
     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, AccountApiException, SubscriptionBaseApiException {
+            throws CatalogApiException {
         final BillingAlignment alignment = catalog.billingAlignment(getPlanPhaseSpecifierFromTransition(catalog, transition), transition.getEffectiveTransitionTime(), subscription.getStartDate());
         return BillCycleDayCalculator.calculateBcdForAlignment(bcdCache, subscription, baseSubscription, alignment, internalTenantContext, accountBillCycleDayLocal);
     }
@@ -281,7 +291,7 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
         });
     }
 
-    private List<Tag> getTagsForObjectType(final ObjectType objectType, final List<Tag> tags, final @Nullable UUID objectId) {
+    private List<Tag> getTagsForObjectType(final ObjectType objectType, final List<Tag> tags, @Nullable final UUID objectId) {
         return ImmutableList.<Tag>copyOf(Iterables.<Tag>filter(tags,
                                                                   new Predicate<Tag>() {
                                                                       @Override
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 b902c1a..ff29ddf 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
@@ -31,7 +31,6 @@ import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.catalog.api.PriceListSet;
-import org.killbill.billing.entitlement.EntitlementService;
 import org.killbill.billing.entitlement.api.BaseEntitlementWithAddOnsSpecifier;
 import org.killbill.billing.entitlement.api.BlockingStateType;
 import org.killbill.billing.entitlement.api.DefaultBaseEntitlementWithAddOnsSpecifier;
@@ -64,8 +63,8 @@ public class TestDefaultInternalBillingApi extends JunctionTestSuiteWithEmbedded
 
         // Create 2 entitlements, one base with one add-on. All entitlements are ACCOUNT aligned
         final String bundleKey1 = UUID.randomUUID().toString();
-        final EntitlementSpecifier entitlementSpecifierBase1 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null), null);
-        final EntitlementSpecifier entitlementSpecifierAO1 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Cabinet", BillingPeriod.TRIANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, null), null);
+        final EntitlementSpecifier entitlementSpecifierBase1 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null));
+        final EntitlementSpecifier entitlementSpecifierAO1 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Cabinet", BillingPeriod.TRIANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, null));
         final BaseEntitlementWithAddOnsSpecifier specifier1 = new DefaultBaseEntitlementWithAddOnsSpecifier(null, bundleKey1, ImmutableList.of(entitlementSpecifierBase1, entitlementSpecifierAO1), null, null, false);
         testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.CREATE, NextEvent.BLOCK);
         entitlementApi.createBaseEntitlementsWithAddOns(account.getId(),
@@ -86,7 +85,7 @@ public class TestDefaultInternalBillingApi extends JunctionTestSuiteWithEmbedded
         final Account accountNoBCD = accountApi.getAccountById(account.getId(), callContext);
         Assert.assertEquals(accountNoBCD.getBillCycleDayLocal(), (Integer) 0);
 
-        final List<BillingEvent> events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext));
+        List<BillingEvent> events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext));
         Assert.assertEquals(events.size(), 3);
         for (final BillingEvent billingEvent : events) {
             if (billingEvent.getSubscription().getId().equals(entitlements.get(1).getId())) {
@@ -98,15 +97,21 @@ public class TestDefaultInternalBillingApi extends JunctionTestSuiteWithEmbedded
 
         // Verify BCD
         final Account accountWithBCD = accountApi.getAccountById(account.getId(), callContext);
-        Assert.assertEquals(accountWithBCD.getBillCycleDayLocal(), (Integer) 6);
+        Assert.assertEquals(accountWithBCD.getBillCycleDayLocal(), (Integer) 7);
 
         // Verify GET
         final List<Entitlement> entitlementsUpdated = entitlementApi.getAllEntitlementsForAccountId(account.getId(), callContext);
         Assert.assertEquals(entitlementsUpdated.size(), 2);
         Assert.assertEquals(entitlementsUpdated.get(0).getLastActiveProduct().getName(), "Shotgun");
-        Assert.assertEquals(entitlementsUpdated.get(0).getBillCycleDayLocal(), (Integer) 6);
+        Assert.assertEquals(entitlementsUpdated.get(0).getBillCycleDayLocal(), (Integer) 7);
         Assert.assertEquals(entitlementsUpdated.get(1).getLastActiveProduct().getName(), "Cabinet");
-        Assert.assertEquals(entitlementsUpdated.get(1).getBillCycleDayLocal(), (Integer) 6);
+        Assert.assertEquals(entitlementsUpdated.get(1).getBillCycleDayLocal(), (Integer) 7);
+
+        events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext));
+        Assert.assertEquals(events.size(), 3);
+        for (final BillingEvent billingEvent : events) {
+            Assert.assertEquals(billingEvent.getBillCycleDayLocal(), 7);
+        }
     }
 
     // This test was originally for https://github.com/killbill/killbill/issues/123.