killbill-memoizeit
Changes
entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java 28(+27 -1)
subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java 5(+3 -2)
subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java 10(+7 -3)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java 45(+42 -3)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java 31(+23 -8)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java 60(+51 -9)
Details
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
index 5cda4c4..5d2e050 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
@@ -21,6 +21,7 @@ import java.util.UUID;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Plan;
@@ -46,7 +47,7 @@ public interface SubscriptionBase extends Entity, Blockable {
public boolean cancelWithDate(final DateTime requestedDate, final CallContext context)
throws SubscriptionBaseApiException;
- public boolean cancelWithPolicy(final BillingActionPolicy policy, final CallContext context)
+ public boolean cancelWithPolicy(final BillingActionPolicy policy, final DateTimeZone accountTimeZone, int accountBillCycleDayLocal, final CallContext context)
throws SubscriptionBaseApiException;
public boolean uncancel(final CallContext context)
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
index 617049b..9530310 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
@@ -50,7 +50,7 @@ public interface SubscriptionBaseInternalApi {
public List<SubscriptionBaseWithAddOns> createBaseSubscriptionsWithAddOns(UUID accountId, Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifier,
InternalCallContext contextWithValidAccountRecordId) throws SubscriptionBaseApiException;
- public void cancelBaseSubscriptions(Iterable<SubscriptionBase> subscriptions, BillingActionPolicy policy, InternalCallContext context) throws SubscriptionBaseApiException;
+ public void cancelBaseSubscriptions(Iterable<SubscriptionBase> subscriptions, BillingActionPolicy policy, DateTimeZone accountTimeZone, int accountBillCycleDayLocal, InternalCallContext context) throws SubscriptionBaseApiException;
public SubscriptionBaseBundle createBundleForAccount(UUID accountId, String bundleName, InternalCallContext context)
throws SubscriptionBaseApiException;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
index 8b5ca40..54788f8 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
@@ -42,6 +42,7 @@ import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.api.EntitlementApiException;
import org.killbill.billing.entitlement.api.EntitlementSpecifier;
+import org.killbill.billing.entitlement.api.Subscription;
import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.invoice.api.DryRunType;
import org.killbill.billing.invoice.api.Invoice;
@@ -479,4 +480,74 @@ public class TestSubscription extends TestIntegrationBase {
clock.addMonths(1);
assertListenerStatus();
}
+
+
+
+ @Test(groups = "slow")
+ public void testCancelSubscriptionInTrialWith_START_OF_TERM() throws Exception {
+ final LocalDate initialDate = new LocalDate(2015, 9, 1);
+ clock.setDay(initialDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ final Entitlement createdEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(createdEntitlement.getEffectiveStartDate().compareTo(initialDate), 0);
+ assertEquals(createdEntitlement.getEffectiveEndDate(), null);
+ assertListenerStatus();
+
+ // Move clock a bit to make sure START_OF_TERM brings us back to initialDate
+ clock.addDays(5);
+
+ busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.NULL_INVOICE);
+ final Entitlement cancelledEntitlement = createdEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.START_OF_TERM, null, callContext);
+ assertListenerStatus();
+
+ final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(cancelledEntitlement.getId(), callContext);
+
+ assertEquals(subscription.getEffectiveEndDate().compareTo(new LocalDate(2015, 9, 6)), 0);
+ assertEquals(subscription.getBillingEndDate().compareTo(initialDate), 0);
+
+ }
+
+ @Test(groups = "slow")
+ public void testCancelSubscriptionAfterTrialWith_START_OF_TERM() throws Exception {
+ final LocalDate initialDate = new LocalDate(2015, 8, 1);
+ clock.setDay(initialDate);
+
+ Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ final Entitlement createdEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(createdEntitlement.getEffectiveStartDate().compareTo(initialDate), 0);
+ assertEquals(createdEntitlement.getEffectiveEndDate(), null);
+ assertListenerStatus();
+
+ // Move out of trial : 2015-8-31
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ account = accountUserApi.getAccountById(account.getId(), callContext);
+ Assert.assertEquals(account.getBillCycleDayLocal().intValue(), 31);
+
+
+ // Move clock a bit to make sure START_OF_TERM brings us back to last Phase date : 2015-9-5
+ clock.addDays(5);
+
+ busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.INVOICE);
+ final Entitlement cancelledEntitlement = createdEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.START_OF_TERM, null, callContext);
+ assertListenerStatus();
+
+ final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(cancelledEntitlement.getId(), callContext);
+
+ assertEquals(subscription.getEffectiveEndDate().compareTo(new LocalDate(2015, 9, 5)), 0);
+ assertEquals(subscription.getBillingEndDate().compareTo(new LocalDate(2015, 8, 31)), 0);
+
+ }
+
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
index fcc3d52..8ba0800 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
@@ -28,11 +28,15 @@ import org.joda.time.LocalDate;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
+import org.killbill.billing.entitlement.api.Subscription;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.junction.DefaultBlockingState;
@@ -42,6 +46,7 @@ import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
+import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
public class TestWithBCDUpdate extends TestIntegrationBase {
@@ -99,6 +104,17 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
expectedInvoices.clear();
+
+ // Add cancellation with START_OF_TERM to verify BCD update is correctly interpreted
+ clock.addDays(3);
+
+ busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.INVOICE);
+ final Entitlement cancelledEntitlement = baseEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.START_OF_TERM, null, callContext);
+ assertListenerStatus();
+
+ final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(cancelledEntitlement.getId(), callContext);
+ assertEquals(subscription.getEffectiveEndDate().compareTo(new LocalDate(2016, 5, 18)), 0);
+ assertEquals(subscription.getBillingEndDate().compareTo(new LocalDate(2016, 5, 15)), 0);
}
@@ -169,6 +185,16 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 10), new LocalDate(2016, 7, 15), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-41.66")));
invoiceChecker.checkInvoice(invoices.get(4).getId(), callContext, expectedInvoices);
expectedInvoices.clear();
+
+ clock.addDays(3);
+ busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.INVOICE);
+ final Entitlement cancelledEntitlement = baseEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.START_OF_TERM, null, callContext);
+ assertListenerStatus();
+
+ final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(cancelledEntitlement.getId(), callContext);
+ assertEquals(subscription.getEffectiveEndDate().compareTo(new LocalDate(2016, 7, 13)), 0);
+ assertEquals(subscription.getBillingEndDate().compareTo(new LocalDate(2016, 7, 10)), 0);
+
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCancelPolicy.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCancelPolicy.java
index 11a3399..2d21e94 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCancelPolicy.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCancelPolicy.java
@@ -18,9 +18,12 @@ package org.killbill.billing.catalog.rules;
import javax.xml.bind.annotation.XmlElement;
+import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.rules.CaseCancelPolicy;
+import org.killbill.xmlloader.ValidationError;
+import org.killbill.xmlloader.ValidationErrors;
public class DefaultCaseCancelPolicy extends DefaultCasePhase<BillingActionPolicy> implements CaseCancelPolicy {
@@ -38,6 +41,16 @@ public class DefaultCaseCancelPolicy extends DefaultCasePhase<BillingActionPolic
}
@Override
+ public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ if (policy == BillingActionPolicy.START_OF_TERM) {
+ errors.add(new ValidationError("Default catalog START_OF_TERM has not been implemented, such policy can be used during cancellation by overriding policy",
+ catalog.getCatalogURI(), DefaultCaseCancelPolicy.class, ""));
+ }
+ return errors;
+ }
+
+
+ @Override
public BillingActionPolicy getBillingActionPolicy() {
return policy;
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultPlanRules.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultPlanRules.java
index bc7f4ae..827aee9 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultPlanRules.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultPlanRules.java
@@ -192,6 +192,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
cur.getToPriceList() == null) {
foundDefaultCase = true;
}
+ cur.validate(catalog, errors);
}
if (!foundDefaultCase) {
errors.add(new ValidationError("Missing default rule case for plan change", catalog.getCatalogURI(), DefaultPlanRules.class, ""));
@@ -212,6 +213,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
cur.getPriceList() == null) {
foundDefaultCase = true;
}
+ cur.validate(catalog, errors);
}
if (!foundDefaultCase) {
errors.add(new ValidationError("Missing default rule case for plan cancellation", catalog.getCatalogURI(), DefaultPlanRules.class, ""));
@@ -225,6 +227,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
} else {
caseChangePlanAlignmentsSet.add(cur);
}
+ cur.validate(catalog, errors);
}
final HashSet<DefaultCaseCreateAlignment> caseCreateAlignmentsSet = new HashSet<DefaultCaseCreateAlignment>();
@@ -234,6 +237,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
} else {
caseCreateAlignmentsSet.add(cur);
}
+ cur.validate(catalog, errors);
}
final HashSet<DefaultCaseBillingAlignment> caseBillingAlignmentsSet = new HashSet<DefaultCaseBillingAlignment>();
@@ -243,6 +247,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
} else {
caseBillingAlignmentsSet.add(cur);
}
+ cur.validate(catalog, errors);
}
final HashSet<DefaultCasePriceList> casePriceListsSet = new HashSet<DefaultCasePriceList>();
@@ -252,6 +257,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
} else {
casePriceListsSet.add(cur);
}
+ cur.validate(catalog, errors);
}
return errors;
}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
index 2cf1acc..9680a76 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
@@ -500,7 +500,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
try {
// Cancel subscription base first, to correctly compute the add-ons entitlements we need to cancel (see below)
- getSubscriptionBase().cancelWithPolicy(billingPolicy, callContext);
+ getSubscriptionBase().cancelWithPolicy(billingPolicy, eventsStream.getAccountTimeZone(), eventsStream.getDefaultBillCycleDayLocal(), callContext);
} catch (final SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
}
@@ -523,6 +523,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
}
private LocalDate getLocalDateFromEntitlementPolicy(final EntitlementActionPolicy entitlementPolicy) {
+
final LocalDate cancellationDate;
switch (entitlementPolicy) {
case IMMEDIATE:
@@ -541,6 +542,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
return (cancellationDate.compareTo(getEffectiveStartDate()) < 0) ? getEffectiveStartDate() : cancellationDate;
}
+
@Override
public Entitlement changePlan(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
index 2f7ad68..982b932 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
@@ -32,7 +32,9 @@ import javax.annotation.Nullable;
import javax.inject.Inject;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
@@ -75,6 +77,7 @@ import org.killbill.notificationq.api.NotificationQueueService;
import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase implements EntitlementInternalApi {
@@ -95,6 +98,21 @@ public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase imp
@Override
public void cancel(final Iterable<Entitlement> entitlements, @Nullable final LocalDate effectiveDate, final BillingActionPolicy billingPolicy, final Iterable<PluginProperty> properties, final InternalCallContext internalCallContext) throws EntitlementApiException {
+
+ if (!entitlements.iterator().hasNext()) {
+ return;
+ }
+
+ int bcd = 0;
+ DateTimeZone accountTimeZone = null;
+ try {
+ bcd = accountApi.getBCD(entitlements.iterator().next().getAccountId(), internalCallContext);
+ accountTimeZone = accountApi.getImmutableAccountDataByRecordId(internalCallContext. getAccountRecordId(), internalCallContext).getTimeZone();
+ } catch (final AccountApiException e) {
+ throw new EntitlementApiException(e);
+ }
+ Preconditions.checkState(bcd > 0 && accountTimeZone != null, "Unexpected condition where account info could not be retrieved");
+
final CallContext callContext = internalCallContextFactory.createCallContext(internalCallContext);
final ImmutableMap.Builder<BlockingState, Optional<UUID>> blockingStates = new ImmutableMap.Builder<BlockingState, Optional<UUID>>();
@@ -141,6 +159,8 @@ public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase imp
final Callable<Void> preCallbacksCallback = new BulkSubscriptionBaseCancellation(subscriptions,
billingPolicy,
+ accountTimeZone,
+ bcd,
internalCallContext);
pluginExecution.executeWithPlugin(preCallbacksCallback, callbacks, pluginContexts);
@@ -182,20 +202,26 @@ public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase imp
private final Iterable<SubscriptionBase> subscriptions;
private final BillingActionPolicy billingPolicy;
+ private final DateTimeZone accountTimeZone;
+ private final int accountBillCycleDayLocal;
private final InternalCallContext callContext;
public BulkSubscriptionBaseCancellation(final Iterable<SubscriptionBase> subscriptions,
final BillingActionPolicy billingPolicy,
+ final DateTimeZone accountTimeZone,
+ final int accountBillCycleDayLocal,
final InternalCallContext callContext) {
this.subscriptions = subscriptions;
this.billingPolicy = billingPolicy;
+ this.accountTimeZone = accountTimeZone;
+ this.accountBillCycleDayLocal = accountBillCycleDayLocal;
this.callContext = callContext;
}
@Override
public Void call() throws Exception {
try {
- subscriptionInternalApi.cancelBaseSubscriptions(subscriptions, billingPolicy, callContext);
+ subscriptionInternalApi.cancelBaseSubscriptions(subscriptions, billingPolicy, accountTimeZone, accountBillCycleDayLocal, callContext);
} catch (final SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
index 8a75ef0..ea26fcc 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
@@ -21,8 +21,7 @@ package org.killbill.billing.invoice.generator;
import org.joda.time.LocalDate;
import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.BillingPeriod;
-
-import com.google.common.annotations.VisibleForTesting;
+import org.killbill.billing.util.bcd.BillCycleDayCalculator;
public class BillingIntervalDetail {
@@ -32,7 +31,6 @@ public class BillingIntervalDetail {
private final int billingCycleDay;
private final BillingPeriod billingPeriod;
private final BillingMode billingMode;
- private final boolean isMonthBased;
// First date after the startDate aligned with the BCD
private LocalDate firstBillingCycleDate;
// Date up to which we should bill
@@ -56,7 +54,6 @@ public class BillingIntervalDetail {
}
this.billingPeriod = billingPeriod;
this.billingMode = billingMode;
- this.isMonthBased = (billingPeriod.getPeriod().getMonths() | billingPeriod.getPeriod().getYears()) > 0;
computeAll();
}
@@ -70,7 +67,7 @@ public class BillingIntervalDetail {
public LocalDate getFutureBillingDateFor(final int nbPeriod) {
final LocalDate proposedDate = InvoiceDateUtils.advanceByNPeriods(firstBillingCycleDate, billingPeriod, nbPeriod);
- return alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
+ return BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, billingCycleDay, billingPeriod);
}
public LocalDate getLastBillingCycleDate() {
@@ -79,7 +76,7 @@ public class BillingIntervalDetail {
public LocalDate getNextBillingCycleDate() {
final LocalDate proposedDate = lastBillingCycleDate != null ? lastBillingCycleDate.plus(billingPeriod.getPeriod()) : firstBillingCycleDate;
- final LocalDate nextBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
+ final LocalDate nextBillingCycleDate = BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, billingCycleDay, billingPeriod);
return nextBillingCycleDate;
}
@@ -109,7 +106,7 @@ public class BillingIntervalDetail {
while (proposedDate.isBefore(startDate)) {
proposedDate = proposedDate.plus(billingPeriod.getPeriod());
}
- firstBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
+ firstBillingCycleDate = BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, billingCycleDay, billingPeriod);
}
private void calculateEffectiveEndDate() {
@@ -141,7 +138,7 @@ public class BillingIntervalDetail {
nextProposedDate = InvoiceDateUtils.advanceByNPeriods(firstBillingCycleDate, billingPeriod, numberOfPeriods);
numberOfPeriods += 1;
}
- proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
+ proposedDate = BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, billingCycleDay, billingPeriod);
// We honor the endDate as long as it does not go beyond our targetDate (by construction this cannot be after the nextProposedDate neither.
if (endDate != null && !endDate.isAfter(targetDate)) {
@@ -171,7 +168,7 @@ public class BillingIntervalDetail {
proposedDate = InvoiceDateUtils.advanceByNPeriods(firstBillingCycleDate, billingPeriod, numberOfPeriods);
numberOfPeriods += 1;
}
- proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
+ proposedDate = BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, billingCycleDay, billingPeriod);
// The proposedDate is greater to our endDate => return it
if (endDate != null && endDate.isBefore(proposedDate)) {
@@ -199,7 +196,7 @@ public class BillingIntervalDetail {
// Our proposed date is billingCycleDate prior to the effectiveEndDate
proposedDate = proposedDate.minus(billingPeriod.getPeriod());
- proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
+ proposedDate = BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, billingCycleDay, billingPeriod);
if (proposedDate.isBefore(firstBillingCycleDate)) {
// Make sure not to go too far in the past
@@ -208,20 +205,4 @@ public class BillingIntervalDetail {
lastBillingCycleDate = proposedDate;
}
}
-
- //
- // We start from a billCycleDate
- //
- private static LocalDate alignProposedBillCycleDate(final LocalDate proposedDate, final int billingCycleDay, final boolean isMonthBased) {
- // billingCycleDay alignment only makes sense for month based BillingPeriod (MONTHLY, QUARTERLY, BIANNUAL, ANNUAL)
- if (!isMonthBased) {
- return proposedDate;
- }
- final int lastDayOfMonth = proposedDate.dayOfMonth().getMaximumValue();
- int proposedBillCycleDate = proposedDate.getDayOfMonth();
- if (proposedBillCycleDate < billingCycleDay && billingCycleDay <= lastDayOfMonth) {
- proposedBillCycleDate = billingCycleDay;
- }
- return new LocalDate(proposedDate.getYear(), proposedDate.getMonthOfYear(), proposedBillCycleDate, proposedDate.getChronology());
- }
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
index e3ad715..83c8117 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
@@ -22,6 +22,7 @@ import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
@@ -56,10 +57,10 @@ public interface SubscriptionBaseApiService {
public boolean cancelWithRequestedDate(DefaultSubscriptionBase subscription, DateTime requestedDate, CallContext context)
throws SubscriptionBaseApiException;
- public boolean cancelWithPolicy(DefaultSubscriptionBase subscription, BillingActionPolicy policy, CallContext context)
+ public boolean cancelWithPolicy(DefaultSubscriptionBase subscription, BillingActionPolicy policy, DateTimeZone accountTimeZone, int accountBillCycleDayLocal, CallContext context)
throws SubscriptionBaseApiException;
- public boolean cancelWithPolicyNoValidation(Iterable<DefaultSubscriptionBase> subscriptions, BillingActionPolicy policy, InternalCallContext context)
+ public boolean cancelWithPolicyNoValidation(Iterable<DefaultSubscriptionBase> subscriptions, BillingActionPolicy policy, DateTimeZone accountTimeZone, int accountBillCycleDayLocal, InternalCallContext context)
throws SubscriptionBaseApiException;
public boolean uncancel(DefaultSubscriptionBase subscription, CallContext context)
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index d8bcd5c..798a653 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -313,7 +313,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
}
@Override
- public void cancelBaseSubscriptions(final Iterable<SubscriptionBase> subscriptions, final BillingActionPolicy policy, final InternalCallContext context) throws SubscriptionBaseApiException {
+ public void cancelBaseSubscriptions(final Iterable<SubscriptionBase> subscriptions, final BillingActionPolicy policy, final DateTimeZone accountTimeZone, int accountBillCycleDayLocal, final InternalCallContext context) throws SubscriptionBaseApiException {
apiService.cancelWithPolicyNoValidation(Iterables.<SubscriptionBase, DefaultSubscriptionBase>transform(subscriptions,
new Function<SubscriptionBase, DefaultSubscriptionBase>() {
@Override
@@ -326,6 +326,8 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
}
}),
policy,
+ accountTimeZone,
+ accountBillCycleDayLocal,
context);
}
@@ -656,7 +658,8 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
final PlanChangeResult planChangeResult = apiService.getPlanChangeResult(subscriptionForChange, inputSpec, utcNow, tenantContext);
policy = planChangeResult.getPolicy();
}
- changeEffectiveDate = subscriptionForChange.getPlanChangeEffectiveDate(policy);
+ // We pass null for billingAlignment, accountTimezone, account BCD because this is not available which means that dryRun with START_OF_TERM BillingPolicy will fail
+ changeEffectiveDate = subscriptionForChange.getPlanChangeEffectiveDate(policy, null, null, -1, context);
}
dryRunEvents = apiService.getEventsOnChangePlan(subscriptionForChange, plan, plan.getPriceListName(), changeEffectiveDate, utcNow, true, context);
break;
@@ -673,7 +676,8 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
subscriptionForCancellation.getCurrentPhase().getPhaseType());
policy = catalogService.getFullCatalog(true, true, context).planCancelPolicy(spec, utcNow);
}
- cancelEffectiveDate = subscriptionForCancellation.getPlanChangeEffectiveDate(policy);
+ // We pass null for billingAlignment, accountTimezone, account BCD because this is not available which means that dryRun with START_OF_TERM BillingPolicy will fail
+ cancelEffectiveDate = subscriptionForCancellation.getPlanChangeEffectiveDate(policy, null, null, -1, context);
}
dryRunEvents = apiService.getEventsOnCancelPlan(subscriptionForCancellation, cancelEffectiveDate, utcNow, true, context);
break;
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
index 90a581e..91acbb2 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
@@ -29,7 +29,12 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
+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;
@@ -57,11 +62,13 @@ import org.killbill.billing.subscription.events.phase.PhaseEvent;
import org.killbill.billing.subscription.events.user.ApiEvent;
import org.killbill.billing.subscription.events.user.ApiEventType;
import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.bcd.BillCycleDayCalculator;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.clock.Clock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
@@ -235,8 +242,8 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
}
@Override
- public boolean cancelWithPolicy(final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
- return apiService.cancelWithPolicy(this, policy, context);
+ public boolean cancelWithPolicy(final BillingActionPolicy policy, final DateTimeZone accountTimeZone, int accountBillCycleDayLocal, final CallContext context) throws SubscriptionBaseApiException {
+ return apiService.cancelWithPolicy(this, policy, accountTimeZone, accountBillCycleDayLocal, context);
}
@Override
@@ -526,13 +533,45 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
return getFutureEndDate() != null;
}
- public DateTime getPlanChangeEffectiveDate(final BillingActionPolicy policy) {
+ public DateTime getPlanChangeEffectiveDate(final BillingActionPolicy policy, @Nullable final BillingAlignment alignment, @Nullable final DateTimeZone accountTimeZone, @Nullable final Integer accountBillCycleDayLocal, final InternalTenantContext context) {
final DateTime candidateResult;
switch (policy) {
case IMMEDIATE:
candidateResult = clock.getUTCNow();
break;
+ case START_OF_TERM:
+ if (chargedThroughDate == null) {
+ candidateResult = getStartDate();
+ // Will take care of billing IN_ARREAR or subscriptions that are not invoiced up to date
+ } else if (!chargedThroughDate.isAfter(clock.getUTCNow())) {
+ candidateResult = chargedThroughDate;
+ } else {
+
+ // In certain path (dryRun, or default catalog START_OF_TERM policy), the info is not easily available and as a result, such policy is not implemented
+ Preconditions.checkState(alignment != null && accountTimeZone != null && accountBillCycleDayLocal != null, "START_OF_TERM not implemented in dryRun use case");
+
+ Preconditions.checkState(alignment != BillingAlignment.BUNDLE || category != ProductCategory.ADD_ON, "START_OF_TERM not implemented for AO configured with a BUNDLE billing alignment");
+
+ // If BCD was overriden at the subscription level, we take its latest value (it should also be reflected in the chargedThroughDate) but still required for
+ // alignment purpose
+ Integer bcd = getBillCycleDayLocal();
+ if (bcd == null) {
+ bcd = BillCycleDayCalculator.calculateBcdForAlignment(null, this, this, alignment, accountTimeZone, accountBillCycleDayLocal);
+ }
+
+ final BillingPeriod billingPeriod = getLastActivePlan().getRecurringBillingPeriod();
+ DateTime proposedDate = chargedThroughDate;
+ while (proposedDate.isAfter(clock.getUTCNow())) {
+ proposedDate = proposedDate.minus(billingPeriod.getPeriod());
+ }
+
+ final LocalDate resultingLocalDate = BillCycleDayCalculator.alignProposedBillCycleDate(proposedDate, bcd, billingPeriod, accountTimeZone);
+ candidateResult = context.toUTCDateTime(resultingLocalDate);
+ }
+
+ break;
+
case END_OF_TERM:
//
// If we have a chargedThroughDate that is 'up to date' we use it, if not default to now
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
index 49efce2..19731b9 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
@@ -29,12 +29,17 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
import org.joda.time.ReadableInstant;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.account.api.ImmutableAccountInternalApi;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingAlignment;
import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogEntity;
@@ -71,6 +76,7 @@ import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.clock.Clock;
import org.killbill.clock.DefaultClock;
+import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -204,7 +210,10 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
try {
final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
final BillingActionPolicy policy = catalogService.getFullCatalog(true, true, internalCallContext).planCancelPolicy(planPhase, now);
- final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy);
+
+ Preconditions.checkState(policy != BillingActionPolicy.START_OF_TERM, "A default START_OF_TERM policy is not availaible");
+
+ final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy, null, null, -1, null);
return doCancelPlan(ImmutableMap.<DefaultSubscriptionBase, DateTime>of(subscription, effectiveDate), now, internalCallContext);
} catch (final CatalogApiException e) {
@@ -226,24 +235,30 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
}
@Override
- public boolean cancelWithPolicy(final DefaultSubscriptionBase subscription, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
+ public boolean cancelWithPolicy(final DefaultSubscriptionBase subscription, final BillingActionPolicy policy, final DateTimeZone accountTimeZone, int accountBillCycleDayLocal, final CallContext context) throws SubscriptionBaseApiException {
final EntitlementState currentState = subscription.getState();
if (currentState == EntitlementState.CANCELLED) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_CANCEL_BAD_STATE, subscription.getId(), currentState);
}
final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
- return cancelWithPolicyNoValidation(ImmutableList.<DefaultSubscriptionBase>of(subscription), policy, internalCallContext);
+ return cancelWithPolicyNoValidation(ImmutableList.<DefaultSubscriptionBase>of(subscription), policy, accountTimeZone, accountBillCycleDayLocal, internalCallContext);
}
@Override
- public boolean cancelWithPolicyNoValidation(final Iterable<DefaultSubscriptionBase> subscriptions, final BillingActionPolicy policy, final InternalCallContext context) throws SubscriptionBaseApiException {
+ public boolean cancelWithPolicyNoValidation(final Iterable<DefaultSubscriptionBase> subscriptions, final BillingActionPolicy policy, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal, final InternalCallContext context) throws SubscriptionBaseApiException {
final Map<DefaultSubscriptionBase, DateTime> subscriptionsWithEffectiveDate = new HashMap<DefaultSubscriptionBase, DateTime>();
final DateTime now = clock.getUTCNow();
- for (final DefaultSubscriptionBase subscription : subscriptions) {
- final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy);
- subscriptionsWithEffectiveDate.put(subscription, effectiveDate);
+ try {
+
+ for (final DefaultSubscriptionBase subscription : subscriptions) {
+ final BillingAlignment billingAlignment = (subscription.getState() == EntitlementState.PENDING ? null : catalogService.getFullCatalog(true, true, context).billingAlignment(new PlanPhaseSpecifier(subscription.getLastActivePlan().getName(), subscription.getLastActivePhase().getPhaseType()), clock.getUTCNow()));
+ final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy, billingAlignment, accountTimeZone, accountBillCycleDayLocal, context);
+ subscriptionsWithEffectiveDate.put(subscription, effectiveDate);
+ }
+ } catch (final CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
}
return doCancelPlan(subscriptionsWithEffectiveDate, now, context);
@@ -330,7 +345,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
}
if (policyMaybeNull != null) {
- return subscription.getPlanChangeEffectiveDate(policyMaybeNull);
+ return subscription.getPlanChangeEffectiveDate(policyMaybeNull, null, null, -1, null);
} else if (requestedDateWithMs != null) {
return DefaultClock.truncateMs(requestedDateWithMs);
} else {
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
index 056208c..af4ddbb 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
@@ -20,16 +20,9 @@ import java.util.UUID;
import org.joda.time.DateTime;
import org.joda.time.Interval;
-import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
-import org.killbill.billing.entity.EntityPersistenceException;
-import org.killbill.billing.subscription.engine.dao.SubscriptionEventSqlDao;
-import org.killbill.billing.subscription.engine.dao.model.SubscriptionEventModelDao;
-import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
-import org.skife.jdbi.v2.Handle;
-import org.testng.Assert;
-import org.testng.annotations.Test;
-
+import org.joda.time.LocalDate;
import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Duration;
import org.killbill.billing.catalog.api.PhaseType;
@@ -37,8 +30,17 @@ import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.entity.EntityPersistenceException;
import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.api.SubscriptionBillingApiException;
+import org.killbill.billing.subscription.engine.dao.SubscriptionEventSqlDao;
+import org.killbill.billing.subscription.engine.dao.model.SubscriptionEventModelDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.skife.jdbi.v2.Handle;
+import org.testng.Assert;
+import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
@@ -344,6 +346,46 @@ public class TestUserApiCancel extends SubscriptionTestSuiteWithEmbeddedDB {
final SubscriptionBaseTransition previousTransition = subscription.getPreviousTransition();
Assert.assertEquals(previousTransition.getPreviousState(), EntitlementState.ACTIVE);
Assert.assertNotNull(previousTransition.getPreviousPlan());
+ }
+
+ @Test(groups = "slow")
+ public void testCancelSubscription_START_OF_TERM() throws SubscriptionBaseApiException {
+
+ // Set date in such a way that Phase align with the first of the month (and so matches our hardcoded accountData account BCD)
+ final DateTime testStartDate = new DateTime(2016, 11, 1, 0, 3, 42, 0);
+ clock.setDeltaFromReality(testStartDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final String prod = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final String planSet = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE
+ DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, prod, term, planSet);
+ PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // Move out of TRIAL
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ // Artificially set the CTD
+ final Duration ctd = testUtil.getDurationMonth(1);
+ final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(clock.getUTCNow(), ctd);
+ subscriptionInternalApi.setChargedThroughDate(subscription.getId(), newChargedThroughDate, internalCallContext);
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+
+ // Move ahead a bit abd cancel START_OF_TERM
+ clock.addDays(5);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ subscription.cancelWithPolicy(BillingActionPolicy.START_OF_TERM, accountData.getTimeZone(), accountData.getBillCycleDayLocal(), callContext);
+ assertListenerStatus();
+
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+ Assert.assertEquals(subscription.getAllTransitions().get(subscription.getAllTransitions().size() - 1).getTransitionType(), SubscriptionBaseTransitionType.CANCEL);
+ Assert.assertEquals(new LocalDate(subscription.getAllTransitions().get(subscription.getAllTransitions().size() - 1).getEffectiveTransitionTime(), accountData.getTimeZone()), new LocalDate(2016, 12, 1));
}
+
+
}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
index 0c2e6c5..cd663dd 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
@@ -151,7 +151,7 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
subscription = subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
- subscription.cancelWithPolicy(BillingActionPolicy.END_OF_TERM, callContext);
+ subscription.cancelWithPolicy(BillingActionPolicy.END_OF_TERM, null, -1, callContext);
try {
subscription.changePlanWithDate(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, clock.getUTCNow(), callContext);
} catch (final SubscriptionBaseApiException e) {
diff --git a/util/src/main/java/org/killbill/billing/util/bcd/BillCycleDayCalculator.java b/util/src/main/java/org/killbill/billing/util/bcd/BillCycleDayCalculator.java
index 51a8e01..80fda69 100644
--- a/util/src/main/java/org/killbill/billing/util/bcd/BillCycleDayCalculator.java
+++ b/util/src/main/java/org/killbill/billing/util/bcd/BillCycleDayCalculator.java
@@ -20,9 +20,13 @@ package org.killbill.billing.util.bcd;
import java.util.Map;
import java.util.UUID;
+import javax.annotation.Nullable;
+
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
import org.killbill.billing.catalog.api.BillingAlignment;
+import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.clock.ClockUtil;
import org.slf4j.Logger;
@@ -32,7 +36,7 @@ public abstract class BillCycleDayCalculator {
private static final Logger log = LoggerFactory.getLogger(BillCycleDayCalculator.class);
- public static int calculateBcdForAlignment(final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final BillingAlignment alignment, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal) {
+ public static int calculateBcdForAlignment(@Nullable final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final BillingAlignment alignment, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal) {
int result = 0;
switch (alignment) {
case ACCOUNT:
@@ -48,11 +52,33 @@ public abstract class BillCycleDayCalculator {
return result;
}
- private static int calculateOrRetrieveBcdFromSubscription(final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final DateTimeZone accountTimeZone) {
- Integer result = bcdCache.get(subscription.getId());
+ public static LocalDate alignProposedBillCycleDate(final LocalDate proposedDate, final int billingCycleDay, final BillingPeriod billingPeriod) {
+ // billingCycleDay alignment only makes sense for month based BillingPeriod (MONTHLY, QUARTERLY, BIANNUAL, ANNUAL)
+ final boolean isMonthBased = (billingPeriod.getPeriod().getMonths() | billingPeriod.getPeriod().getYears()) > 0;
+ if (!isMonthBased) {
+ return proposedDate;
+ }
+ final int lastDayOfMonth = proposedDate.dayOfMonth().getMaximumValue();
+ int proposedBillCycleDate = proposedDate.getDayOfMonth();
+ if (proposedBillCycleDate < billingCycleDay && billingCycleDay <= lastDayOfMonth) {
+ proposedBillCycleDate = billingCycleDay;
+ }
+ return new LocalDate(proposedDate.getYear(), proposedDate.getMonthOfYear(), proposedBillCycleDate, proposedDate.getChronology());
+ }
+
+ public static LocalDate alignProposedBillCycleDate(final DateTime proposedDate, final int billingCycleDay, final BillingPeriod billingPeriod, final DateTimeZone accountTimeZone) {
+ final LocalDate proposedLocalDate = ClockUtil.toLocalDate(proposedDate, accountTimeZone);
+ final LocalDate resultingLocalDate = alignProposedBillCycleDate(proposedLocalDate, billingCycleDay, billingPeriod);
+ return resultingLocalDate;
+ }
+
+ private static int calculateOrRetrieveBcdFromSubscription(@Nullable final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final DateTimeZone accountTimeZone) {
+ Integer result = bcdCache != null ? bcdCache.get(subscription.getId()) : null;
if (result == null) {
result = calculateBcdFromSubscription(subscription, accountTimeZone);
- bcdCache.put(subscription.getId(), result);
+ if (bcdCache != null) {
+ bcdCache.put(subscription.getId(), result);
+ }
}
return result;
}
diff --git a/util/src/test/java/org/killbill/billing/mock/MockSubscription.java b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
index c3287d1..5475050 100644
--- a/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
+++ b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
@@ -20,6 +20,7 @@ import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.mockito.Mockito;
@@ -71,9 +72,9 @@ public class MockSubscription implements SubscriptionBase {
}
@Override
- public boolean cancelWithPolicy(BillingActionPolicy policy, CallContext context)
+ public boolean cancelWithPolicy(BillingActionPolicy policy, final DateTimeZone accountTimeZone, int accountBillCycleDayLocal, CallContext context)
throws SubscriptionBaseApiException {
- return sub.cancelWithPolicy(policy, context);
+ return sub.cancelWithPolicy(policy, accountTimeZone, accountBillCycleDayLocal, context);
}
@Override