killbill-memoizeit
Changes
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java 14(+12 -2)
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).