killbill-uncached
Changes
api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java 45(+23 -22)
entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java 2(+1 -1)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java 9(+5 -4)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java 60(+41 -19)
junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java 2(+1 -1)
junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java 52(+27 -25)
junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java 4(+2 -2)
pom.xml 2(+1 -1)
subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java 3(+2 -1)
subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java 37(+32 -5)
subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java 210(+148 -62)
subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java 7(+5 -2)
subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionApiService.java 10(+6 -4)
subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java 2(+1 -1)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java 189(+124 -65)
subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java 30(+22 -8)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java 33(+28 -5)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/RepairSubscriptionDao.java 2(+1 -1)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java 2(+1 -1)
subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java 8(+5 -3)
subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java 2(+1 -1)
subscription/src/test/java/org/killbill/billing/subscription/api/migration/TestMigration.java 26(+13 -13)
subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java 26(+12 -14)
Details
diff --git a/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java b/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java
index f8cda97..0f9e8fa 100644
--- a/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java
@@ -18,15 +18,13 @@ package org.killbill.billing.junction;
import java.util.UUID;
-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.invoice.api.DryRunArguments;
public interface BillingInternalApi {
/**
* @return an ordered list of billing event for the given accounts
*/
- public BillingEventSet getBillingEventsForAccountAndUpdateAccountBCD(UUID accountId, InternalCallContext context);
+ public BillingEventSet getBillingEventsForAccountAndUpdateAccountBCD(UUID accountId, DryRunArguments dryRunArguments, InternalCallContext 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 4522bf6..245f81a 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
@@ -29,53 +29,54 @@ import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
import org.killbill.billing.util.entity.Pagination;
public interface SubscriptionBaseInternalApi {
- public SubscriptionBase createSubscription(final UUID bundleId, final PlanPhaseSpecifier spec, final DateTime requestedDateWithMs,
- final InternalCallContext context) throws SubscriptionBaseApiException;
+ public SubscriptionBase createSubscription(UUID bundleId, PlanPhaseSpecifier spec, DateTime requestedDateWithMs,
+ InternalCallContext context) throws SubscriptionBaseApiException;
- public SubscriptionBaseBundle createBundleForAccount(final UUID accountId, final String bundleName, final InternalCallContext context)
+ public SubscriptionBaseBundle createBundleForAccount(UUID accountId, String bundleName, InternalCallContext context)
throws SubscriptionBaseApiException;
- public List<SubscriptionBaseBundle> getBundlesForAccountAndKey(final UUID accountId, final String bundleKey, final InternalTenantContext context)
+ public List<SubscriptionBaseBundle> getBundlesForAccountAndKey(UUID accountId, String bundleKey, InternalTenantContext context)
throws SubscriptionBaseApiException;
- public List<SubscriptionBaseBundle> getBundlesForAccount(final UUID accountId, final InternalTenantContext context);
+ public List<SubscriptionBaseBundle> getBundlesForAccount(UUID accountId, InternalTenantContext context);
- public List<SubscriptionBaseBundle> getBundlesForKey(final String bundleKey, final InternalTenantContext context);
+ public List<SubscriptionBaseBundle> getBundlesForKey(String bundleKey, InternalTenantContext context);
- public Pagination<SubscriptionBaseBundle> getBundles(final Long offset, final Long limit, final InternalTenantContext context);
+ public Pagination<SubscriptionBaseBundle> getBundles(Long offset, Long limit, InternalTenantContext context);
- public Pagination<SubscriptionBaseBundle> searchBundles(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context);
+ public Pagination<SubscriptionBaseBundle> searchBundles(String searchKey, Long offset, Long limit, InternalTenantContext context);
- public Iterable<UUID> getNonAOSubscriptionIdsForKey(final String bundleKey, final InternalTenantContext context);
+ public Iterable<UUID> getNonAOSubscriptionIdsForKey(String bundleKey, InternalTenantContext context);
- public List<SubscriptionBase> getSubscriptionsForBundle(final UUID bundleId, final InternalTenantContext context);
+ public List<SubscriptionBase> getSubscriptionsForBundle(UUID bundleId, DryRunArguments dryRunArguments, InternalTenantContext context)
+ throws SubscriptionBaseApiException;
- public Map<UUID, List<SubscriptionBase>> getSubscriptionsForAccount(final InternalTenantContext context);
+ public Map<UUID, List<SubscriptionBase>> getSubscriptionsForAccount(InternalTenantContext context);
- public SubscriptionBase getBaseSubscription(final UUID bundleId, final InternalTenantContext context) throws SubscriptionBaseApiException;
+ public SubscriptionBase getBaseSubscription(UUID bundleId, InternalTenantContext context) throws SubscriptionBaseApiException;
- public SubscriptionBase getSubscriptionFromId(final UUID id, final InternalTenantContext context) throws SubscriptionBaseApiException;
+ public SubscriptionBase getSubscriptionFromId(UUID id, InternalTenantContext context) throws SubscriptionBaseApiException;
- public SubscriptionBaseBundle getBundleFromId(final UUID id, final InternalTenantContext context) throws SubscriptionBaseApiException;
+ public SubscriptionBaseBundle getBundleFromId(UUID id, InternalTenantContext context) throws SubscriptionBaseApiException;
- public UUID getAccountIdFromSubscriptionId(final UUID subscriptionId, final InternalTenantContext context) throws SubscriptionBaseApiException;
+ public UUID getAccountIdFromSubscriptionId(UUID subscriptionId, InternalTenantContext context) throws SubscriptionBaseApiException;
- public void setChargedThroughDate(final UUID subscriptionId, final DateTime chargedThruDate, final InternalCallContext context);
+ public void setChargedThroughDate(UUID subscriptionId, DateTime chargedThruDate, InternalCallContext context);
- public List<EffectiveSubscriptionInternalEvent> getAllTransitions(final SubscriptionBase subscription, final InternalTenantContext context);
+ public List<EffectiveSubscriptionInternalEvent> getAllTransitions(SubscriptionBase subscription, InternalTenantContext context);
- public List<EffectiveSubscriptionInternalEvent> getBillingTransitions(final SubscriptionBase subscription, final InternalTenantContext context);
+ public List<EffectiveSubscriptionInternalEvent> getBillingTransitions(SubscriptionBase subscription, InternalTenantContext context);
- public DateTime getNextBillingDate(final UUID accountId, final InternalTenantContext context);
+ public List<EntitlementAOStatusDryRun> getDryRunChangePlanStatus(UUID subscriptionId, @Nullable String baseProductName,
+ DateTime requestedDate, InternalTenantContext context) throws SubscriptionBaseApiException;
- public List<EntitlementAOStatusDryRun> getDryRunChangePlanStatus(final UUID subscriptionId, @Nullable final String baseProductName,
- final DateTime requestedDate, final InternalTenantContext context) throws SubscriptionBaseApiException;
+ public void updateExternalKey(UUID bundleId, String newExternalKey, InternalCallContext context);
- public void updateExternalKey(final UUID bundleId, final String newExternalKey, final InternalCallContext context);
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
index 11e9f1b..9982a0a 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
@@ -17,6 +17,7 @@
package org.killbill.billing.beatrix.integration;
import java.math.BigDecimal;
+import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -28,6 +29,7 @@ import org.killbill.billing.account.api.AccountData;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
import org.killbill.billing.beatrix.util.PaymentChecker.ExpectedPaymentCheck;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.PriceListSet;
@@ -37,7 +39,9 @@ import org.killbill.billing.entitlement.api.Entitlement;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.api.SubscriptionBundle;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
@@ -62,37 +66,60 @@ public class TestIntegration extends TestIntegrationBase {
// Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
clock.setDay(new LocalDate(2012, 4, 1));
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+
//
// CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
//
+
+ TestDryRunArguments dryRun = new TestDryRunArguments("Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, null, null,
+ SubscriptionEventType.START_BILLING, null, null, clock.getUTCNow(), null);
+ Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), dryRun, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+
final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
// Check bundle after BP got created otherwise we get an error from auditApi.
subscriptionChecker.checkSubscriptionCreated(bpSubscription.getId(), internalCallContext);
- invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext, expectedInvoices);
+ expectedInvoices.clear();
+
//
// ADD ADD_ON ON THE SAME DAY
//
+ dryRun = new TestDryRunArguments("Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, null, null,
+ SubscriptionEventType.START_BILLING, null, bpSubscription.getBundleId(), clock.getUTCNow(), null);
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), dryRun, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("399.95")));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+
addAOEntitlementAndCheckForCompletion(bpSubscription.getBundleId(), "Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE, NextEvent.PAYMENT);
- Invoice invoice = invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("399.95")));
+ Invoice invoice = invoiceChecker.checkInvoice(account.getId(), 2, callContext, expectedInvoices);
paymentChecker.checkPayment(account.getId(), 1, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 4, 1), new BigDecimal("399.95"), TransactionStatus.SUCCESS, invoice.getId(), Currency.USD));
+ expectedInvoices.clear();
//
// CANCEL BP ON THE SAME DAY (we should have two cancellations, BP and AO)
// There is no invoice created as we only adjust the previous invoice.
//
+ dryRun = new TestDryRunArguments(null, null, null, null, null, SubscriptionEventType.STOP_BILLING, bpSubscription.getId(),
+ bpSubscription.getBundleId(), clock.getUTCNow(), null);
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), dryRun, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-399.95")));
+ // The second invoice should be adjusted for the AO (we paid for the full period) and since we paid we should also see a CBA
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 4, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("399.95"),
+ false /* Avoid checking dates for CBA because code is using context and context createdDate is wrong in the test as we reset the clock too late, bummer... */ ));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+
+
cancelEntitlementAndCheckForCompletion(bpSubscription, clock.getUTCNow(), NextEvent.BLOCK, NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.CANCEL, NextEvent.INVOICE);
- invoiceChecker.checkInvoice(account.getId(), 2,
- callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("399.95")));
invoiceChecker.checkInvoice(account.getId(), 3,
callContext,
- // The second invoice should be adjusted for the AO (we paid for the full period) and since we paid we should also see a CBA
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-399.95")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 4, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("399.95")));
+ expectedInvoices);
checkNoMoreInvoiceToGenerate(account);
-
}
@Test(groups = "slow")
@@ -108,6 +135,8 @@ public class TestIntegration extends TestIntegrationBase {
clock.setTime(initialCreationDate);
int invoiceItemCount = 1;
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+
//
// CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
//
@@ -120,9 +149,17 @@ public class TestIntegration extends TestIntegrationBase {
//
// CHANGE PLAN IMMEDIATELY AND EXPECT BOTH EVENTS: NextEvent.CHANGE NextEvent.INVOICE
//
+ TestDryRunArguments dryRun = new TestDryRunArguments("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, null, null, SubscriptionEventType.CHANGE,
+ subscription.getId(), subscription.getBundleId(), clock.getUTCNow(), null);
+ Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), dryRun, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(initialCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+
+
changeEntitlementAndCheckForCompletion(baseEntitlement, "Assault-Rifle", BillingPeriod.MONTHLY, null, NextEvent.CHANGE, NextEvent.INVOICE);
- invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, new ExpectedInvoiceItemCheck(initialCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, expectedInvoices);
invoiceChecker.checkChargedThroughDate(subscription.getId(), clock.getUTCToday(), callContext);
+ expectedInvoices.clear();
//
// MOVE 4 * TIME THE CLOCK
@@ -130,10 +167,19 @@ public class TestIntegration extends TestIntegrationBase {
setDateAndCheckForCompletion(new DateTime(2012, 2, 28, 23, 59, 59, 0, testTimeZone));
setDateAndCheckForCompletion(new DateTime(2012, 2, 29, 23, 59, 59, 0, testTimeZone));
setDateAndCheckForCompletion(new DateTime(2012, 3, 1, 23, 59, 59, 0, testTimeZone));
- setDateAndCheckForCompletion(new DateTime(2012, 3, 2, 23, 59, 59, 0, testTimeZone), NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
- invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 3, 2),
- new LocalDate(2012, 3, 31), InvoiceItemType.RECURRING, new BigDecimal("561.24")));
+
+ DateTime nextDate = clock.getUTCNow().plusDays(1);
+ dryRun = new TestDryRunArguments();
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), new LocalDate(nextDate, testTimeZone), dryRun, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2012, 3, 2), new LocalDate(2012, 3, 31), InvoiceItemType.RECURRING, new BigDecimal("561.24")));
+
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+
+
+ setDateAndCheckForCompletion(nextDate, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+ invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, expectedInvoices);
invoiceChecker.checkChargedThroughDate(subscription.getId(), new LocalDate(2012, 3, 31), callContext);
+ expectedInvoices.clear();
//
// CHANGE PLAN EOT AND EXPECT NOTHING
@@ -146,9 +192,19 @@ public class TestIntegration extends TestIntegrationBase {
//
final LocalDate firstRecurringPistolDate = subscription.getChargedThroughDate().toLocalDate();
final LocalDate secondRecurringPistolDate = firstRecurringPistolDate.plusMonths(1);
+
+
+ nextDate = clock.getUTCNow().plusDays(31);
+ dryRun = new TestDryRunArguments();
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), new LocalDate(nextDate, testTimeZone), dryRun, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2012, 3, 31), new LocalDate(2012, 4, 30), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+
addDaysAndCheckForCompletion(31, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT);
- invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 3, 31), new LocalDate(2012, 4, 30), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, expectedInvoices);
invoiceChecker.checkChargedThroughDate(subscription.getId(), secondRecurringPistolDate, callContext);
+ expectedInvoices.clear();
//
// MOVE 3 * TIME AFTER NEXT BILL CYCLE DAY AND EXPECT EVENT : NextEvent.INVOICE, NextEvent.PAYMENT
@@ -222,6 +278,19 @@ public class TestIntegration extends TestIntegrationBase {
//
// CHANGE PLAN EOT AND EXPECT NOTHING
//
+
+
+ TestDryRunArguments dryRun = new TestDryRunArguments("Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, null, null, SubscriptionEventType.CHANGE,
+ subscription.getId(), subscription.getBundleId(), null, null);
+ try {
+ invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), dryRun, callContext);
+ Assert.fail("Call should return no invoices");
+ } catch (InvoiceApiException e) {
+ System.out.print("foo");
+
+ }
+
+
baseEntitlement = changeEntitlementAndCheckForCompletion(baseEntitlement, "Pistol", BillingPeriod.MONTHLY, null);
subscription = subscriptionDataFromSubscription(baseEntitlement.getSubscriptionBase());
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index 02c99b0..cdd9e5a 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -49,6 +49,7 @@ import org.killbill.billing.beatrix.util.SubscriptionChecker;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
@@ -57,9 +58,10 @@ import org.killbill.billing.entitlement.api.Entitlement;
import org.killbill.billing.entitlement.api.EntitlementApi;
import org.killbill.billing.entitlement.api.EntitlementApiException;
import org.killbill.billing.entitlement.api.SubscriptionApi;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
+import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
-import org.killbill.billing.invoice.api.InvoicePayment;
import org.killbill.billing.invoice.api.InvoicePaymentApi;
import org.killbill.billing.invoice.api.InvoiceService;
import org.killbill.billing.invoice.api.InvoiceUserApi;
@@ -296,7 +298,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
protected void checkNoMoreInvoiceToGenerate(final Account account) {
try {
- invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), false, callContext);
+ invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), null, callContext);
fail("Should not have generated an extra invoice");
} catch (final InvoiceApiException e) {
assertEquals(e.getCode(), ErrorCode.INVOICE_NOTHING_TO_DO.getCode());
@@ -407,7 +409,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
}, events);
}
- protected Payment createPaymentAndCheckForCompletion(final Account account, final Invoice invoice, final BigDecimal amount, final Currency currency, final NextEvent... events) {
+ protected Payment createPaymentAndCheckForCompletion(final Account account, final Invoice invoice, final BigDecimal amount, final Currency currency, final NextEvent... events) {
return doCallAndCheckForCompletion(new Function<Void, Payment>() {
@Override
public Payment apply(@Nullable final Void input) {
@@ -435,7 +437,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
final PluginProperty prop1 = new PluginProperty(InvoicePaymentControlPluginApi.PROP_IPCD_INVOICE_ID, invoice.getId().toString(), false);
properties.add(prop1);
- return paymentApi.createPurchaseWithPaymentControl(account, account.getPaymentMethodId(), null, invoice.getBalance(), invoice.getCurrency(), UUID.randomUUID().toString(),
+ return paymentApi.createPurchaseWithPaymentControl(account, account.getPaymentMethodId(), null, invoice.getBalance(), invoice.getCurrency(), UUID.randomUUID().toString(),
UUID.randomUUID().toString(), properties, PAYMENT_OPTIONS, callContext);
} catch (final PaymentApiException e) {
fail(e.toString());
@@ -682,8 +684,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
assertEquals(tags.size(), 1);
}
-
- protected void remove_AUTO_PAY_OFF_Tag(final UUID id, final ObjectType type, final NextEvent...additionalEvents) throws TagDefinitionApiException, TagApiException {
+ protected void remove_AUTO_PAY_OFF_Tag(final UUID id, final ObjectType type, final NextEvent... additionalEvents) throws TagDefinitionApiException, TagApiException {
busHandler.pushExpectedEvent(NextEvent.TAG);
busHandler.pushExpectedEvents(additionalEvents);
tagUserApi.removeTag(id, type, ControlTagType.AUTO_PAY_OFF.getId(), callContext);
@@ -702,4 +703,71 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
log.debug(" ************ DONE WITH BUS HANDLER CHECK ********************");
return result;
}
+
+ protected static class TestDryRunArguments implements DryRunArguments {
+
+ private final PlanPhaseSpecifier spec;
+ private final SubscriptionEventType action;
+ private final UUID subscriptionId;
+ private final UUID bundleId;
+ private final DateTime effectiveDate;
+ private final BillingActionPolicy billingPolicy;
+
+ public TestDryRunArguments() {
+ this.spec = null;
+ this.action = null;
+ this.subscriptionId = null;
+ this.bundleId = null;
+ this.effectiveDate = null;
+ this.billingPolicy = null;
+ }
+
+ public TestDryRunArguments(final String productName,
+ final ProductCategory category,
+ final BillingPeriod billingPeriod,
+ final String priceList,
+ final PhaseType phaseType,
+ final SubscriptionEventType action,
+ final UUID subscriptionId,
+ final UUID bundleId,
+ final DateTime effectiveDate,
+ final BillingActionPolicy billingPolicy) {
+ this.spec = new PlanPhaseSpecifier(productName, category, billingPeriod, priceList, phaseType);
+ this.action = action;
+ this.subscriptionId = subscriptionId;
+ this.bundleId = bundleId;
+ this.effectiveDate = effectiveDate;
+ this.billingPolicy = billingPolicy;
+ }
+
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return spec;
+ }
+
+ @Override
+ public SubscriptionEventType getAction() {
+ return action;
+ }
+
+ @Override
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ @Override
+ public BillingActionPolicy getBillingActionPolicy() {
+ return billingPolicy;
+ }
+ }
}
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 8c73b48..3dc6b0e 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
@@ -18,10 +18,12 @@
package org.killbill.billing.beatrix.integration;
import java.math.BigDecimal;
+import java.util.ArrayList;
import java.util.List;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.testng.annotations.Test;
import org.killbill.billing.account.api.Account;
@@ -76,21 +78,24 @@ public class TestSubscription extends TestIntegrationBase {
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
//
// FORCE AN IMMEDIATE CHANGE OF THE BILLING PERIOD
//
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("169.32")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2334.20")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("2164.88"), false /* Issue with test where created date for context is wrong*/));
+
+ TestDryRunArguments dryRun = new TestDryRunArguments(productName, ProductCategory.BASE, BillingPeriod.MONTHLY, null, null,
+ SubscriptionEventType.CHANGE, bpEntitlement.getId(), bpEntitlement.getBundleId(), null, BillingActionPolicy.IMMEDIATE);
+ Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), dryRun, callContext);
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, toBeChecked);
+
changeEntitlementAndCheckForCompletion(bpEntitlement, productName, BillingPeriod.MONTHLY, BillingActionPolicy.IMMEDIATE, NextEvent.CHANGE, NextEvent.INVOICE);
invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
assertEquals(invoices.size(), 3);
- toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
- invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
- toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("169.32")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2334.20")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("2164.88")));
invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
//
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/util/InvoiceChecker.java b/beatrix/src/test/java/org/killbill/billing/beatrix/util/InvoiceChecker.java
index 5fe963b..d3f24c2 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/util/InvoiceChecker.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/util/InvoiceChecker.java
@@ -20,16 +20,7 @@ import java.math.BigDecimal;
import java.util.List;
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.joda.time.LocalTime;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.Assert;
-
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.entitlement.api.EntitlementApi;
import org.killbill.billing.entitlement.api.EntitlementApiException;
@@ -40,13 +31,15 @@ import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoiceUserApi;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.util.callcontext.CallContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.inject.Inject;
-import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
@@ -92,6 +85,10 @@ public class InvoiceChecker {
public void checkInvoice(final UUID invoiceId, final CallContext context, final List<ExpectedInvoiceItemCheck> expected) throws InvoiceApiException {
final Invoice invoice = invoiceUserApi.getInvoice(invoiceId, context);
Assert.assertNotNull(invoice);
+ checkInvoice(invoice, context, expected);
+ }
+
+ public void checkInvoiceNoAudits(final Invoice invoice, final CallContext context, final List<ExpectedInvoiceItemCheck> expected) throws InvoiceApiException {
final List<InvoiceItem> actual = invoice.getInvoiceItems();
Assert.assertEquals(actual.size(), expected.size());
@@ -156,6 +153,11 @@ public class InvoiceChecker {
Assert.fail(failureMessage);
}
}
+
+ }
+
+ public void checkInvoice(final Invoice invoice, final CallContext context, final List<ExpectedInvoiceItemCheck> expected) throws InvoiceApiException {
+ checkInvoiceNoAudits(invoice, context, expected);
auditChecker.checkInvoiceCreated(invoice, context);
}
@@ -191,23 +193,32 @@ public class InvoiceChecker {
private final LocalDate startDate;
private final LocalDate endDate;
private final InvoiceItemType type;
- private final BigDecimal Amount;
+ private final BigDecimal amount;
- public ExpectedInvoiceItemCheck(final InvoiceItemType type, final BigDecimal amount) {
- this.checkDates = false;
+ public ExpectedInvoiceItemCheck(final InvoiceItemType type, final BigDecimal amount, boolean checkDates) {
+ this.checkDates = checkDates;
this.type = type;
this.startDate = null;
this.endDate = null;
- Amount = amount;
+ this.amount = amount;
+ }
+
+ public ExpectedInvoiceItemCheck(final InvoiceItemType type, final BigDecimal amount) {
+ this(type, amount, false);
}
public ExpectedInvoiceItemCheck(final LocalDate startDate, final LocalDate endDate,
- final InvoiceItemType type, final BigDecimal amount) {
- this.checkDates = true;
+ final InvoiceItemType type, final BigDecimal amount, boolean checkDates) {
+ this.checkDates = checkDates;
this.startDate = startDate;
this.endDate = endDate;
this.type = type;
- Amount = amount;
+ this.amount = amount;
+ }
+
+ public ExpectedInvoiceItemCheck(final LocalDate startDate, final LocalDate endDate,
+ final InvoiceItemType type, final BigDecimal amount) {
+ this(startDate, endDate, type, amount, true);
}
public boolean shouldCheckDates() {
@@ -227,7 +238,7 @@ public class InvoiceChecker {
}
public BigDecimal getAmount() {
- return Amount;
+ return amount;
}
}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
index 3475b66..d74f611 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
@@ -360,7 +360,7 @@ public class DefaultEntitlementApi implements EntitlementApi {
// Block all associated subscriptions - TODO Do we want to block the bundle as well (this will add an extra STOP_ENTITLEMENT event in the bundle timeline stream)?
// Note that there is no un-transfer at the moment, so we effectively add a blocking state on disk for all subscriptions
- for (final SubscriptionBase subscriptionBase : subscriptionBaseInternalApi.getSubscriptionsForBundle(baseBundle.getId(), contextWithValidAccountRecordId)) {
+ for (final SubscriptionBase subscriptionBase : subscriptionBaseInternalApi.getSubscriptionsForBundle(baseBundle.getId(), null, contextWithValidAccountRecordId)) {
final BlockingState blockingState = new DefaultBlockingState(subscriptionBase.getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, requestedDate);
entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(blockingState, contextWithValidAccountRecordId);
}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java
index d7246fc..835fd6c 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java
@@ -224,7 +224,7 @@ public class EventsStreamBuilder {
try {
subscription = subscriptionInternalApi.getSubscriptionFromId(entitlementId, internalTenantContext);
bundle = subscriptionInternalApi.getBundleFromId(subscription.getBundleId(), internalTenantContext);
- allSubscriptionsForBundle = subscriptionInternalApi.getSubscriptionsForBundle(subscription.getBundleId(), internalTenantContext);
+ allSubscriptionsForBundle = subscriptionInternalApi.getSubscriptionsForBundle(subscription.getBundleId(), null, internalTenantContext);
baseSubscription = Iterables.<SubscriptionBase>tryFind(allSubscriptionsForBundle,
new Predicate<SubscriptionBase>() {
@Override
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
index 34474c8..a8b7364 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -34,6 +34,7 @@ import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.invoice.InvoiceDispatcher;
+import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
@@ -199,8 +200,9 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
}));
}
+
@Override
- public Invoice triggerInvoiceGeneration(final UUID accountId, final LocalDate targetDate, final boolean dryRun,
+ public Invoice triggerInvoiceGeneration(final UUID accountId, final LocalDate targetDate, final DryRunArguments dryRunArguments,
final CallContext context) throws InvoiceApiException {
final InternalCallContext internalContext = internalCallContextFactory.createInternalCallContext(accountId, context);
@@ -212,7 +214,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
}
final DateTime processingDateTime = targetDate.toDateTimeAtCurrentTime(account.getTimeZone());
- final Invoice result = dispatcher.processAccount(accountId, processingDateTime, dryRun, internalContext);
+ final Invoice result = dispatcher.processAccount(accountId, processingDateTime, dryRunArguments, internalContext);
if (result == null) {
throw new InvoiceApiException(ErrorCode.INVOICE_NOTHING_TO_DO, accountId, targetDate);
} else {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index edc9b8a..6e2c67a 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -40,12 +40,15 @@ import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingMode;
+import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.Usage;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.events.BusInternalEvent;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
import org.killbill.billing.events.InvoiceAdjustmentInternalEvent;
import org.killbill.billing.events.InvoiceInternalEvent;
+import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
@@ -149,7 +152,7 @@ public class InvoiceDispatcher {
return;
}
final UUID accountId = subscriptionApi.getAccountIdFromSubscriptionId(subscriptionId, context);
- processAccount(accountId, targetDate, false, context);
+ processAccount(accountId, targetDate, null, context);
} catch (final SubscriptionBaseApiException e) {
log.error("Failed handling SubscriptionBase change.",
new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, subscriptionId.toString()));
@@ -157,12 +160,12 @@ public class InvoiceDispatcher {
}
public Invoice processAccount(final UUID accountId, final DateTime targetDate,
- final boolean dryRun, final InternalCallContext context) throws InvoiceApiException {
+ @Nullable final DryRunArguments dryRunArguments, final InternalCallContext context) throws InvoiceApiException {
GlobalLock lock = null;
try {
lock = locker.lockWithNumberOfTries(LockerType.ACCOUNT_FOR_INVOICE_PAYMENTS.toString(), accountId.toString(), NB_LOCK_TRY);
- return processAccountWithLock(accountId, targetDate, dryRun, context);
+ return processAccountWithLock(accountId, targetDate, dryRunArguments, context);
} catch (final LockFailedException e) {
// Not good!
log.error(String.format("Failed to process invoice for account %s, targetDate %s",
@@ -176,11 +179,13 @@ public class InvoiceDispatcher {
}
private Invoice processAccountWithLock(final UUID accountId, final DateTime targetDateTime,
- final boolean dryRun, final InternalCallContext context) throws InvoiceApiException {
+ @Nullable final DryRunArguments dryRunArguments, final InternalCallContext context) throws InvoiceApiException {
+
+ final boolean isDryRun = dryRunArguments != null;
try {
// Make sure to first set the BCD if needed then get the account object (to have the BCD set)
- final BillingEventSet billingEvents = billingApi.getBillingEventsForAccountAndUpdateAccountBCD(accountId, context);
+ final BillingEventSet billingEvents = billingApi.getBillingEventsForAccountAndUpdateAccountBCD(accountId, dryRunArguments, context);
final Account account = accountApi.getAccountById(accountId, context);
final DateAndTimeZoneContext dateAndTimeZoneContext = billingEvents.iterator().hasNext() ?
@@ -207,7 +212,7 @@ public class InvoiceDispatcher {
//
if (invoice == null) {
log.info("Generated null invoice for accountId {} and targetDate {} (targetDateTime {})", new Object[]{accountId, targetDate, targetDateTime});
- if (!dryRun) {
+ if (!isDryRun) {
final BusInternalEvent event = new DefaultNullInvoiceEvent(accountId, clock.getUTCToday(),
context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
postEvent(event, accountId, context);
@@ -240,7 +245,7 @@ public class InvoiceDispatcher {
}
boolean isRealInvoiceWithItems = false;
- if (!dryRun) {
+ if (!isDryRun) {
// Extract the set of invoiceId for which we see items that don't belong to current generated invoice
final Set<UUID> adjustedUniqueOtherInvoiceId = new TreeSet<UUID>();
@@ -296,7 +301,7 @@ public class InvoiceDispatcher {
}
}
- if (account.isNotifiedForInvoices() && isRealInvoiceWithItems && !dryRun) {
+ if (account.isNotifiedForInvoices() && isRealInvoiceWithItems && !isDryRun) {
// Need to re-hydrate the invoice object to get the invoice number (record id)
// API_FIX InvoiceNotifier public API?
invoiceNotifier.notify(account, new DefaultInvoice(invoiceDao.getById(invoice.getId(), context)), buildTenantContext(context));
@@ -443,5 +448,4 @@ public class InvoiceDispatcher {
}
}
}
-
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
index ed949df..74ef87f 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
@@ -65,7 +65,7 @@ public class InvoiceListener {
try {
final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "RepairBundle", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
- dispatcher.processAccount(event.getAccountId(), event.getEffectiveDate(), false, context);
+ dispatcher.processAccount(event.getAccountId(), event.getEffectiveDate(), null, context);
} catch (InvoiceApiException e) {
log.error(e.getMessage());
}
@@ -94,7 +94,7 @@ public class InvoiceListener {
try {
final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "SubscriptionBaseTransition", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
- dispatcher.processAccount(event.getAccountId(), event.getEffectiveTransitionTime(), false, context);
+ dispatcher.processAccount(event.getAccountId(), event.getEffectiveTransitionTime(), null, context);
} catch (InvoiceApiException e) {
log.error(e.getMessage());
}
@@ -111,7 +111,7 @@ public class InvoiceListener {
try {
final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "SubscriptionBaseTransition", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
final UUID accountId = accountApi.getByRecordId(event.getSearchKey1(), context);
- dispatcher.processAccount(accountId, clock.getUTCNow(), false, context);
+ dispatcher.processAccount(accountId, clock.getUTCNow(), null, context);
} catch (InvoiceApiException e) {
log.error(e.getMessage());
} catch (AccountApiException e) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
index 7d0d5c6..e293356 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
@@ -63,7 +63,7 @@ public class InvoiceTagHandler {
private void processUnpaid_AUTO_INVOICING_OFF_invoices(final UUID accountId, final InternalCallContext context) {
try {
- dispatcher.processAccount(accountId, clock.getUTCNow(), false, context);
+ dispatcher.processAccount(accountId, clock.getUTCNow(), null, context);
} catch (InvoiceApiException e) {
log.warn(String.format("Failed to process process removal AUTO_INVOICING_OFF for account %s", accountId), e);
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
index cf40b6d..c184a59 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
@@ -38,6 +38,9 @@ import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
+import org.killbill.billing.invoice.TestInvoiceHelper.DryRunFutureDateArguments;
+import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
@@ -51,6 +54,7 @@ import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.util.timezone.DateAndTimeZoneContext;
import org.killbill.clock.ClockMock;
+import org.mockito.Mock;
import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
@@ -85,7 +89,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
fixedPrice, BigDecimal.ONE, currency, BillingPeriod.MONTHLY, 1,
BillingMode.IN_ADVANCE, "", 1L, SubscriptionBaseTransitionType.CREATE));
- Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
+ Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<DryRunArguments>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
final DateTime target = new DateTime();
@@ -94,21 +98,21 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
nonEntityDao, invoiceNotifier, locker, busService.getBus(),
clock, controllerDispatcher);
- Invoice invoice = dispatcher.processAccount(accountId, target, true, context);
+ Invoice invoice = dispatcher.processAccount(accountId, target, new DryRunFutureDateArguments(), context);
Assert.assertNotNull(invoice);
List<InvoiceModelDao> invoices = invoiceDao.getInvoicesByAccount(context);
Assert.assertEquals(invoices.size(), 0);
// Try it again to double check
- invoice = dispatcher.processAccount(accountId, target, true, context);
+ invoice = dispatcher.processAccount(accountId, target, new DryRunFutureDateArguments(), context);
Assert.assertNotNull(invoice);
invoices = invoiceDao.getInvoicesByAccount(context);
Assert.assertEquals(invoices.size(), 0);
// This time no dry run
- invoice = dispatcher.processAccount(accountId, target, false, context);
+ invoice = dispatcher.processAccount(accountId, target, null, context);
Assert.assertNotNull(invoice);
invoices = invoiceDao.getInvoicesByAccount(context);
@@ -141,13 +145,13 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
new MockPlanPhase(jetTrialEvergreen1000USD, PhaseType.EVERGREEN), null, new BigDecimal("1000"), account.getCurrency(), BillingPeriod.MONTHLY,
31, BillingMode.IN_ADVANCE, "CHANGE", 3L, SubscriptionBaseTransitionType.CHANGE));
- Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
+ Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<DryRunArguments>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(pluginRegistry, generator, accountApi, billingApi, subscriptionApi, invoiceDao,
nonEntityDao, invoiceNotifier, locker, busService.getBus(),
clock, controllerDispatcher);
- final Invoice invoice = dispatcher.processAccount(account.getId(), new DateTime("2012-07-30T00:00:00.000Z"), false, context);
+ final Invoice invoice = dispatcher.processAccount(account.getId(), new DateTime("2012-07-30T00:00:00.000Z"), null, context);
Assert.assertNotNull(invoice);
final List<InvoiceItem> invoiceItems = invoice.getInvoiceItems();
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 955e9a0..407e59f 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -38,13 +38,17 @@ import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
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.BillingMode;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.Usage;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.entity.EntityPersistenceException;
+import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
@@ -194,14 +198,14 @@ public class TestInvoiceHelper {
fixedPrice, BigDecimal.ONE, currency, BillingPeriod.MONTHLY, 1,
BillingMode.IN_ADVANCE, "", 1L, SubscriptionBaseTransitionType.CREATE));
- Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
+ Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<DryRunArguments>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(pluginRegistry, generator, accountApi, billingApi, subscriptionApi,
invoiceDao, nonEntityDao, invoiceNotifier, locker, busService.getBus(),
clock, cacheControllerDispatcher);
- Invoice invoice = dispatcher.processAccount(account.getId(), targetDate, true, internalCallContext);
+ Invoice invoice = dispatcher.processAccount(account.getId(), targetDate, new DryRunFutureDateArguments(), internalCallContext);
Assert.assertNotNull(invoice);
final InternalCallContext context = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
@@ -209,7 +213,7 @@ public class TestInvoiceHelper {
List<InvoiceModelDao> invoices = invoiceDao.getInvoicesByAccount(context);
Assert.assertEquals(invoices.size(), 0);
- invoice = dispatcher.processAccount(account.getId(), targetDate, false, context);
+ invoice = dispatcher.processAccount(account.getId(), targetDate, null, context);
Assert.assertNotNull(invoice);
invoices = invoiceDao.getInvoicesByAccount(context);
@@ -420,4 +424,34 @@ public class TestInvoiceHelper {
}
};
}
+ public static class DryRunFutureDateArguments implements DryRunArguments {
+ public DryRunFutureDateArguments() {
+ }
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return null;
+ }
+ @Override
+ public SubscriptionEventType getAction() {
+ return null;
+ }
+ @Override
+ public UUID getSubscriptionId() {
+ return null;
+ }
+ @Override
+ public DateTime getEffectiveDate() {
+ return null;
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return null;
+ }
+
+ @Override
+ public BillingActionPolicy getBillingActionPolicy() {
+ return null;
+ }
+ }
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceDryRunJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceDryRunJson.java
new file mode 100644
index 0000000..3dbd829
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceDryRunJson.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2014 Groupon, Inc
+ * Copyright 2014 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
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class InvoiceDryRunJson {
+
+ private final String dryRunAction;
+ private final String phaseType;
+ private final String productName;
+ private final String productCategory;
+ private final String billingPeriod;
+ private final String priceListName;
+ private final LocalDate effectiveDate;
+ private final String subscriptionId;
+ private final String bundleId;
+ private final String billingPolicy;
+
+ @JsonCreator
+ public InvoiceDryRunJson(@JsonProperty("dryRunAction") @Nullable final String dryRunAction,
+ @JsonProperty("phaseType") @Nullable final String phaseType,
+ @JsonProperty("productName") @Nullable final String productName,
+ @JsonProperty("productCategory") @Nullable final String productCategory,
+ @JsonProperty("billingPeriod") @Nullable final String billingPeriod,
+ @JsonProperty("priceListName") @Nullable final String priceListName,
+ @JsonProperty("subscriptionId") @Nullable final String subscriptionId,
+ @JsonProperty("bundleId") @Nullable final String bundleId,
+ @JsonProperty("effectiveDate") @Nullable final LocalDate effectiveDate,
+ @JsonProperty("billingPolicy") @Nullable final String billingPolicy) {
+ this.dryRunAction = dryRunAction;
+ this.phaseType = phaseType;
+ this.productName = productName;
+ this.productCategory = productCategory;
+ this.billingPeriod = billingPeriod;
+ this.priceListName = priceListName;
+ this.subscriptionId = subscriptionId;
+ this.bundleId = bundleId;
+ this.effectiveDate = effectiveDate;
+ this.billingPolicy = billingPolicy;
+ }
+
+ public String getDryRunAction() {
+ return dryRunAction;
+ }
+
+ public String getPhaseType() {
+ return phaseType;
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public String getProductCategory() {
+ return productCategory;
+ }
+
+ public String getBillingPeriod() {
+ return billingPeriod;
+ }
+
+ public String getPriceListName() {
+ return priceListName;
+ }
+
+ public String getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ public LocalDate getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public String getBundleId() {
+ return bundleId;
+ }
+
+ public String getBillingPolicy() {
+ return billingPolicy;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof InvoiceDryRunJson)) {
+ return false;
+ }
+
+ final InvoiceDryRunJson that = (InvoiceDryRunJson) o;
+
+ if (billingPeriod != null ? !billingPeriod.equals(that.billingPeriod) : that.billingPeriod != null) {
+ return false;
+ }
+ if (billingPolicy != null ? !billingPolicy.equals(that.billingPolicy) : that.billingPolicy != null) {
+ return false;
+ }
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+ return false;
+ }
+ if (dryRunAction != null ? !dryRunAction.equals(that.dryRunAction) : that.dryRunAction != null) {
+ return false;
+ }
+ if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
+ return false;
+ }
+ if (phaseType != null ? !phaseType.equals(that.phaseType) : that.phaseType != null) {
+ return false;
+ }
+ if (priceListName != null ? !priceListName.equals(that.priceListName) : that.priceListName != null) {
+ return false;
+ }
+ if (productCategory != null ? !productCategory.equals(that.productCategory) : that.productCategory != null) {
+ return false;
+ }
+ if (productName != null ? !productName.equals(that.productName) : that.productName != null) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = dryRunAction != null ? dryRunAction.hashCode() : 0;
+ result = 31 * result + (phaseType != null ? phaseType.hashCode() : 0);
+ result = 31 * result + (productName != null ? productName.hashCode() : 0);
+ result = 31 * result + (productCategory != null ? productCategory.hashCode() : 0);
+ result = 31 * result + (billingPeriod != null ? billingPeriod.hashCode() : 0);
+ result = 31 * result + (priceListName != null ? priceListName.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ result = 31 * result + (subscriptionId != null ? subscriptionId.hashCode() : 0);
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (billingPolicy != null ? billingPolicy.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
index 9399f63..bebfd31 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
@@ -30,6 +30,7 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -45,14 +46,23 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.SubscriptionApiException;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
+import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
@@ -60,6 +70,7 @@ import org.killbill.billing.invoice.api.InvoiceNotifier;
import org.killbill.billing.invoice.api.InvoicePayment;
import org.killbill.billing.invoice.api.InvoiceUserApi;
import org.killbill.billing.jaxrs.json.CustomFieldJson;
+import org.killbill.billing.jaxrs.json.InvoiceDryRunJson;
import org.killbill.billing.jaxrs.json.InvoiceItemJson;
import org.killbill.billing.jaxrs.json.InvoiceJson;
import org.killbill.billing.jaxrs.json.InvoicePaymentJson;
@@ -81,6 +92,7 @@ import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.clock.Clock;
+import org.killbill.clock.ClockUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -254,7 +266,6 @@ public class InvoiceResource extends JaxRsResourceBase {
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id or target datetime supplied")})
public Response createFutureInvoice(@QueryParam(QUERY_ACCOUNT_ID) final String accountId,
@QueryParam(QUERY_TARGET_DATE) final String targetDateTime,
- @QueryParam(QUERY_DRY_RUN) @DefaultValue("false") final Boolean dryRun,
@HeaderParam(HDR_CREATED_BY) final String createdBy,
@HeaderParam(HDR_REASON) final String reason,
@HeaderParam(HDR_COMMENT) final String comment,
@@ -263,12 +274,67 @@ public class InvoiceResource extends JaxRsResourceBase {
final CallContext callContext = context.createContext(createdBy, reason, comment, request);
final LocalDate inputDate = toLocalDate(UUID.fromString(accountId), targetDateTime, callContext);
- final Invoice generatedInvoice = invoiceApi.triggerInvoiceGeneration(UUID.fromString(accountId), inputDate, dryRun,
- callContext);
- if (dryRun) {
- return Response.status(Status.OK).entity(new InvoiceJson(generatedInvoice)).build();
- } else {
+ try {
+ final Invoice generatedInvoice = invoiceApi.triggerInvoiceGeneration(UUID.fromString(accountId), inputDate, null,
+ callContext);
return uriBuilder.buildResponse(uriInfo, InvoiceResource.class, "getInvoice", generatedInvoice.getId());
+ } catch (InvoiceApiException e) {
+ if (e.getCode() == ErrorCode.INVOICE_NOTHING_TO_DO.getCode()) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+ throw e;
+ }
+ }
+
+ @Timed
+ @POST
+ @Path("/" + DRY_RUN)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Generate a dryRun invoice", response = InvoiceJson.class)
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id or target datetime supplied")})
+ public Response generateDryRunInvoice(@Nullable final InvoiceDryRunJson dryRunSubscriptionSpec,
+ @QueryParam(QUERY_ACCOUNT_ID) final String accountId,
+ @QueryParam(QUERY_TARGET_DATE) final String targetDateTime,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws AccountApiException, InvoiceApiException {
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+ final LocalDate inputDate = toLocalDate(UUID.fromString(accountId), targetDateTime, callContext);
+
+ // Passing a null or empty body means we are trying to generate an invoice with a (future) targetDate
+ // On the other hand if body is not null, we are attempting a dryRun subscription operation
+ if (dryRunSubscriptionSpec != null && dryRunSubscriptionSpec.getDryRunAction() != null) {
+ if (SubscriptionEventType.START_BILLING.toString().equals(dryRunSubscriptionSpec.getDryRunAction())) {
+ verifyNonNullOrEmpty(dryRunSubscriptionSpec.getProductName(), "DryRun subscription product category should be specified");
+ verifyNonNullOrEmpty(dryRunSubscriptionSpec.getBillingPeriod(), "DryRun subscription billingPeriod should be specified");
+ verifyNonNullOrEmpty(dryRunSubscriptionSpec.getProductCategory(), "DryRun subscription product category should be specified");
+ if (dryRunSubscriptionSpec.getProductCategory().equals(ProductCategory.ADD_ON)) {
+ verifyNonNullOrEmpty(dryRunSubscriptionSpec.getBundleId(), "DryRun bundle ID should be specified");
+ }
+ } else if (SubscriptionEventType.CHANGE.toString().equals(dryRunSubscriptionSpec.getDryRunAction())) {
+ verifyNonNullOrEmpty(dryRunSubscriptionSpec.getProductName(), "DryRun subscription product category should be specified");
+ verifyNonNullOrEmpty(dryRunSubscriptionSpec.getBillingPeriod(), "DryRun subscription billingPeriod should be specified");
+ verifyNonNullOrEmpty(dryRunSubscriptionSpec.getSubscriptionId(), "DryRun subscriptionID should be specified");
+ } else if (SubscriptionEventType.STOP_BILLING.toString().equals(dryRunSubscriptionSpec.getDryRunAction())) {
+ verifyNonNullOrEmpty(dryRunSubscriptionSpec.getSubscriptionId(), "DryRun subscriptionID should be specified");
+ }
+ }
+
+ final Account account = accountUserApi.getAccountById(UUID.fromString(accountId), callContext);
+
+ final DryRunArguments dryRunArguments = new DefaultDryRunArguments(dryRunSubscriptionSpec, account.getTimeZone(), clock);
+ try {
+ final Invoice generatedInvoice = invoiceApi.triggerInvoiceGeneration(UUID.fromString(accountId), inputDate, dryRunArguments,
+ callContext);
+ return Response.status(Status.OK).entity(new InvoiceJson(generatedInvoice, true, null)).build();
+ } catch (InvoiceApiException e) {
+ if (e.getCode() == ErrorCode.INVOICE_NOTHING_TO_DO.getCode()) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+ throw e;
}
}
@@ -603,4 +669,81 @@ public class InvoiceResource extends JaxRsResourceBase {
protected ObjectType getObjectType() {
return ObjectType.INVOICE;
}
+
+ private static class DefaultDryRunArguments implements DryRunArguments {
+
+ private final SubscriptionEventType action;
+ private final UUID subscriptionId;
+ private final DateTime effectiveDate;
+ private final PlanPhaseSpecifier specifier;
+ private final UUID bundleId;
+ private final BillingActionPolicy billingPolicy;
+
+ public DefaultDryRunArguments(final SubscriptionEventType action, final UUID subscriptionId, final UUID bundleId,
+ final PlanPhaseSpecifier specifier, final DateTime effectiveDate, final BillingActionPolicy billingPolicy) {
+ this.action = action;
+ this.subscriptionId = subscriptionId;
+ this.bundleId = bundleId;
+ this.effectiveDate = effectiveDate;
+ this.billingPolicy = billingPolicy;
+ this.specifier = specifier;
+ }
+
+ public DefaultDryRunArguments(final InvoiceDryRunJson input, final DateTimeZone accountTimeZone, final Clock clock) {
+ if (input == null) {
+ this.action = null;
+ this.subscriptionId = null;
+ this.effectiveDate = null;
+ this.specifier = null;
+ this.bundleId = null;
+ this.billingPolicy = null;
+ } else {
+ this.action = input.getDryRunAction() != null ? SubscriptionEventType.valueOf(input.getDryRunAction()) : null;
+ this.subscriptionId = input.getSubscriptionId() != null ? UUID.fromString(input.getSubscriptionId()) : null;
+ this.bundleId = input.getBundleId() != null ? UUID.fromString(input.getBundleId()) : null;
+ this.effectiveDate = input.getEffectiveDate() != null ? ClockUtil.computeDateTimeWithUTCReferenceTime(input.getEffectiveDate(), clock.getUTCNow().toLocalTime(), accountTimeZone, clock) : null;
+ this.billingPolicy = input.getBillingPolicy() != null ? BillingActionPolicy.valueOf(input.getBillingPolicy()) : null;
+ this.specifier = (input.getProductName() != null &&
+ input.getProductCategory() != null &&
+ input.getBillingPeriod() != null) ?
+ new PlanPhaseSpecifier(input.getProductName(),
+ ProductCategory.valueOf(input.getProductCategory()),
+ BillingPeriod.valueOf(input.getBillingPeriod()),
+ input.getPriceListName(),
+ input.getPhaseType() != null ? PhaseType.valueOf(input.getPhaseType()) : null) :
+ null;
+ }
+ }
+
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return specifier;
+ }
+
+ @Override
+ public SubscriptionEventType getAction() {
+ return action;
+ }
+
+ @Override
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ @Override
+ public BillingActionPolicy getBillingActionPolicy() {
+ return billingPolicy;
+ }
+ }
+
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
index a6d97d0..d7c5ab8 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
@@ -65,7 +65,6 @@ public interface JaxrsResource {
public static final String QUERY_CALL_COMPLETION = "callCompletion";
public static final String QUERY_USE_REQUESTED_DATE_FOR_BILLING = "useRequestedDateForBilling";
public static final String QUERY_CALL_TIMEOUT = "callTimeoutSec";
- public static final String QUERY_DRY_RUN = "dryRun";
public static final String QUERY_TARGET_DATE = "targetDate";
public static final String QUERY_BILLING_POLICY = "billingPolicy";
public static final String QUERY_ENTITLEMENT_POLICY = "entitlementPolicy";
@@ -159,6 +158,7 @@ public interface JaxrsResource {
public static final String INVOICE_PAYMENTS = "invoicePayments";
public static final String INVOICE_PAYMENTS_PATH = PREFIX + "/" + INVOICE_PAYMENTS;
+ public static final String DRY_RUN = "dryRun";
public static final String CHARGEBACKS = "chargebacks";
public static final String CHARGEBACKS_PATH = PREFIX + "/" + CHARGEBACKS;
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java
index 4cdfd36..20d06c9 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java
@@ -17,6 +17,7 @@
package org.killbill.billing.junction.plumbing.billing;
import java.util.List;
+import java.util.UUID;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
@@ -60,7 +61,7 @@ public class BillCycleDayCalculator {
this.subscriptionApi = subscriptionApi;
}
- protected int calculateBcd(final SubscriptionBaseBundle bundle, final SubscriptionBase subscription, final EffectiveSubscriptionInternalEvent transition, final Account account, final InternalCallContext context)
+ protected int calculateBcd(final UUID bundleId, final SubscriptionBase subscription, final EffectiveSubscriptionInternalEvent transition, final Account account, final InternalCallContext context)
throws CatalogApiException, AccountApiException, SubscriptionBaseApiException {
final Catalog catalog = catalogService.getFullCatalog();
@@ -85,11 +86,11 @@ public class BillCycleDayCalculator {
phase.getPhaseType()),
transition.getRequestedTransitionTime());
- return calculateBcdForAlignment(alignment, bundle, subscription, account, catalog, plan, context);
+ return calculateBcdForAlignment(alignment, bundleId, subscription, account, catalog, plan, context);
}
@VisibleForTesting
- int calculateBcdForAlignment(final BillingAlignment alignment, final SubscriptionBaseBundle bundle, final SubscriptionBase subscription,
+ int calculateBcdForAlignment(final BillingAlignment alignment, final UUID bundleId, final SubscriptionBase subscription,
final Account account, final Catalog catalog, final Plan plan, final InternalCallContext context) throws AccountApiException, SubscriptionBaseApiException, CatalogApiException {
int result = 0;
switch (alignment) {
@@ -100,7 +101,7 @@ public class BillCycleDayCalculator {
}
break;
case BUNDLE:
- final SubscriptionBase baseSub = subscriptionApi.getBaseSubscription(bundle.getId(), context);
+ final SubscriptionBase baseSub = subscriptionApi.getBaseSubscription(bundleId, context);
Plan basePlan = baseSub.getCurrentPlan();
if (basePlan == null) {
// The BP has been cancelled
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 9b4d9a2..07d0b3b 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
@@ -22,16 +22,6 @@ 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.BillingMode;
-import org.killbill.billing.catalog.api.Usage;
-import org.killbill.billing.util.timezone.DateAndTimeZoneContext;
-import org.killbill.clock.Clock;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
@@ -40,16 +30,23 @@ import org.killbill.billing.account.api.MutableAccountData;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.entitlement.EntitlementTransitionType;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.junction.BillingEventSet;
import org.killbill.billing.junction.BillingInternalApi;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
import org.killbill.billing.tag.TagInternalApi;
import org.killbill.billing.util.tag.ControlTagType;
import org.killbill.billing.util.tag.Tag;
+import org.killbill.clock.Clock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
@@ -84,7 +81,7 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
}
@Override
- public BillingEventSet getBillingEventsForAccountAndUpdateAccountBCD(final UUID accountId, final InternalCallContext context) {
+ public BillingEventSet getBillingEventsForAccountAndUpdateAccountBCD(final UUID accountId, final DryRunArguments dryRunArguments, final InternalCallContext context) {
final List<SubscriptionBaseBundle> bundles = subscriptionApi.getBundlesForAccount(accountId, context);
final DefaultBillingEventSet result = new DefaultBillingEventSet();
result.setRecurrringBillingMode(catalogService.getCurrentCatalog().getRecurringBillingMode());
@@ -100,9 +97,11 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
return result; // billing is off, we are done
}
- addBillingEventsForBundles(bundles, account, context, result);
+ addBillingEventsForBundles(bundles, account, dryRunArguments, context, result);
} catch (AccountApiException e) {
log.warn("Failed while getting BillingEvent", e);
+ } catch (SubscriptionBaseApiException e) {
+ log.warn("Failed while getting BillingEvent", e);
}
// Pretty-print the events, before and after the blocking calculator does its magic
@@ -122,10 +121,29 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
}
}
- private void addBillingEventsForBundles(final List<SubscriptionBaseBundle> bundles, final Account account, final InternalCallContext context,
- final DefaultBillingEventSet result) {
+ private void addBillingEventsForBundles(final List<SubscriptionBaseBundle> bundles, final Account account, final DryRunArguments dryRunArguments, final InternalCallContext context,
+ final DefaultBillingEventSet result) throws SubscriptionBaseApiException {
+
+ final boolean dryRunMode = dryRunArguments != null;
+
+ // 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 &&
+ dryRunArguments.getAction() == SubscriptionEventType.START_BILLING &&
+ dryRunArguments.getBundleId() == null) {
+ final UUID fakeBundleId = UUID.randomUUID();
+ final List<SubscriptionBase> subscriptions = subscriptionApi.getSubscriptionsForBundle(fakeBundleId, dryRunArguments, context);
+
+ addBillingEventsForSubscription(subscriptions, fakeBundleId, account, dryRunMode, context, result);
+
+ }
+
for (final SubscriptionBaseBundle bundle : bundles) {
- final List<SubscriptionBase> subscriptions = subscriptionApi.getSubscriptionsForBundle(bundle.getId(), context);
+ final DryRunArguments dryRunArgumentsForBundle = (dryRunArguments != null &&
+ dryRunArguments.getBundleId() != null &&
+ dryRunArguments.getBundleId().equals(bundle.getId())) ?
+ dryRunArguments : null;
+ final List<SubscriptionBase> subscriptions = subscriptionApi.getSubscriptionsForBundle(bundle.getId(), dryRunArgumentsForBundle, context);
//Check if billing is off for the bundle
final List<Tag> bundleTags = tagApi.getTags(bundle.getId(), ObjectType.BUNDLE, context);
@@ -135,14 +153,18 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
result.getSubscriptionIdsWithAutoInvoiceOff().add(subscription.getId());
}
} else { // billing is not off
- addBillingEventsForSubscription(subscriptions, bundle, account, context, result);
+ addBillingEventsForSubscription(subscriptions, bundle.getId(), account, dryRunMode, context, result);
}
}
}
- private void addBillingEventsForSubscription(final List<SubscriptionBase> subscriptions, final SubscriptionBaseBundle bundle, final Account account, final InternalCallContext context, final DefaultBillingEventSet result) {
+ private void addBillingEventsForSubscription(final List<SubscriptionBase> subscriptions, final UUID bundleId, final Account account,
+ final boolean dryRunMode,
+ final InternalCallContext context,
+ final DefaultBillingEventSet result) {
- boolean updatedAccountBCD = false;
+ // 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;
for (final SubscriptionBase subscription : subscriptions) {
// The subscription did not even start, so there is nothing to do yet, we can skip and avoid some NPE down the line when calculating the BCD
@@ -152,7 +174,7 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
for (final EffectiveSubscriptionInternalEvent transition : subscriptionApi.getBillingTransitions(subscription, context)) {
try {
- final int bcdLocal = bcdCalculator.calculateBcd(bundle, subscription, transition, account, context);
+ final int bcdLocal = bcdCalculator.calculateBcd(bundleId, subscription, transition, account, context);
if (account.getBillCycleDayLocal() == 0 && !updatedAccountBCD) {
final MutableAccountData modifiedData = account.toMutableAccountData();
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java
index 674aae2..5542ff3 100644
--- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java
@@ -62,7 +62,7 @@ public class TestBillCycleDayCalculator extends JunctionTestSuiteNoDB {
final Account account = Mockito.mock(Account.class);
Mockito.when(account.getTimeZone()).thenReturn(accountTimeZone);
- final Integer billCycleDayLocal = billCycleDayCalculator.calculateBcdForAlignment(BillingAlignment.BUNDLE, bundle, subscription,
+ final Integer billCycleDayLocal = billCycleDayCalculator.calculateBcdForAlignment(BillingAlignment.BUNDLE, bundle.getId(), subscription,
account, catalog, null, internalCallContext);
Assert.assertEquals(billCycleDayLocal, (Integer) expectedBCDUTC);
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
index 6cd947f..1180e15 100644
--- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
@@ -16,10 +16,18 @@
package org.killbill.billing.junction.plumbing.billing;
-import com.google.common.collect.ImmutableList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.MockCatalog;
import org.killbill.billing.catalog.api.BillingAlignment;
import org.killbill.billing.catalog.api.BillingMode;
@@ -33,32 +41,26 @@ import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.dao.MockBlockingStateDao;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.invoice.api.DryRunArguments;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingEventSet;
+import org.killbill.billing.junction.DefaultBlockingState;
import org.killbill.billing.junction.JunctionTestSuiteNoDB;
import org.killbill.billing.mock.MockEffectiveSubscriptionEvent;
import org.killbill.billing.mock.MockSubscription;
-import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
import org.killbill.billing.util.api.TagApiException;
-import org.killbill.billing.callcontext.InternalTenantContext;
-import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
-import org.killbill.billing.junction.BillingEvent;
-import org.killbill.billing.junction.BillingEventSet;
-import org.killbill.billing.junction.DefaultBlockingState;
import org.killbill.billing.util.tag.ControlTagType;
import org.killbill.billing.util.tag.dao.MockTagDao;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.SortedSet;
-import java.util.UUID;
+import com.google.common.collect.ImmutableList;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
@@ -90,11 +92,11 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
final List<SubscriptionBase> subscriptions = ImmutableList.<SubscriptionBase>of(subscription);
Mockito.when(subscriptionInternalApi.getBundlesForAccount(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(bundles);
- Mockito.when(subscriptionInternalApi.getSubscriptionsForBundle(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(subscriptions);
+ Mockito.when(subscriptionInternalApi.getSubscriptionsForBundle(Mockito.<UUID>any(), Mockito.<DryRunArguments>any(), Mockito.<InternalTenantContext>any())).thenReturn(subscriptions);
Mockito.when(subscriptionInternalApi.getSubscriptionFromId(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(subscription);
Mockito.when(subscriptionInternalApi.getBundleFromId(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(bundle);
Mockito.when(subscriptionInternalApi.getBaseSubscription(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(subscription);
- Mockito.when(subscriptionInternalApi.getBillingTransitions(Mockito.<SubscriptionBase>any(), Mockito.<InternalTenantContext>any())).thenReturn(effectiveSubscriptionTransitions);
+ Mockito.when(subscriptionInternalApi.getBillingTransitions(Mockito.<SubscriptionBase>any(), Mockito.<InternalTenantContext>any())).thenReturn(effectiveSubscriptionTransitions);
Mockito.when(subscriptionInternalApi.getAllTransitions(Mockito.<SubscriptionBase>any(), Mockito.<InternalTenantContext>any())).thenReturn(effectiveSubscriptionTransitions);
catalog = ((MockCatalog) catalogService.getCurrentCatalog());
@@ -110,7 +112,7 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
@Test(groups = "fast")
public void testBillingEventsEmpty() throws AccountApiException {
- final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(new UUID(0L, 0L), internalCallContext);
+ final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(new UUID(0L, 0L), null, internalCallContext);
Assert.assertEquals(events.size(), 0);
}
@@ -123,7 +125,7 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
final Account account = createAccount(10);
- final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+ final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext);
checkFirstEvent(events, nextPlan, account.getBillCycleDayLocal(), subId, now, nextPhase, SubscriptionBaseTransitionType.CREATE.toString());
}
@@ -137,7 +139,7 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
catalog.setBillingAlignment(BillingAlignment.SUBSCRIPTION);
- final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+ final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext);
// The expected BCD is when the subscription started since we skip the trial phase
checkFirstEvent(events, nextPlan, subscription.getStartDate().getDayOfMonth(), subId, now, nextPhase, SubscriptionBaseTransitionType.CREATE.toString());
}
@@ -150,7 +152,7 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
final Account account = createAccount(32);
- final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+ final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext);
// The expected BCD is the account BCD (account aligned by default)
checkFirstEvent(events, nextPlan, 32, subId, now, nextPhase, SubscriptionBaseTransitionType.CREATE.toString());
}
@@ -166,7 +168,7 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
catalog.setBillingAlignment(BillingAlignment.BUNDLE);
((MockSubscription) subscription).setPlan(catalog.findPlan("PickupTrialEvergreen10USD", now));
- final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+ final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext);
// The expected BCD is when the subscription started
checkFirstEvent(events, nextPlan, subscription.getStartDate().getDayOfMonth(), subId, now, nextPhase, SubscriptionBaseTransitionType.CREATE.toString());
}
@@ -179,10 +181,10 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
final Account account = createAccount(32);
- blockingStateDao.setBlockingState(new DefaultBlockingState(bunId, BlockingStateType.SUBSCRIPTION_BUNDLE, DISABLED_BUNDLE, "test", true, true, true, now.plusDays(1)), clock, internalCallContext);
+ blockingStateDao.setBlockingState(new DefaultBlockingState(bunId, BlockingStateType.SUBSCRIPTION_BUNDLE, DISABLED_BUNDLE, "test", true, true, true, now.plusDays(1)), clock, internalCallContext);
blockingStateDao.setBlockingState(new DefaultBlockingState(bunId, BlockingStateType.SUBSCRIPTION_BUNDLE, CLEAR_BUNDLE, "test", false, false, false, now.plusDays(2)), clock, internalCallContext);
- final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+ final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext);
Assert.assertEquals(events.size(), 3);
final Iterator<BillingEvent> it = events.iterator();
@@ -202,7 +204,7 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
tagInternalApi.addTag(account.getId(), ObjectType.ACCOUNT, ControlTagType.AUTO_INVOICING_OFF.getId(), internalCallContext);
- final BillingEventSet events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+ final BillingEventSet events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext);
assertEquals(events.isAccountAutoInvoiceOff(), true);
assertEquals(events.size(), 0);
@@ -218,7 +220,7 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
tagInternalApi.addTag(bunId, ObjectType.BUNDLE, ControlTagType.AUTO_INVOICING_OFF.getId(), internalCallContext);
- final BillingEventSet events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+ final BillingEventSet events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext);
assertEquals(events.getSubscriptionIdsWithAutoInvoiceOff().size(), 1);
assertEquals(events.getSubscriptionIdsWithAutoInvoiceOff().get(0), subId);
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 4b30b27..5811e10 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
@@ -183,7 +183,7 @@ public class TestDefaultInternalBillingApi extends JunctionTestSuiteWithEmbedded
// * 2013-08-07 to 2013-08-07 (block1Date)
// * 2013-08-12 to 2013-08-12 (block2Date)
// * 2013-08-15 to 2013-10-04 [2013-08-15 to 2013-10-01 (block3Date -> block4Date) and 2013-10-01 to 2013-10-04 (block4Date -> block5Date)]
- final List<BillingEvent> events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext));
+ final List<BillingEvent> events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext));
Assert.assertEquals(events.size(), 7);
Assert.assertEquals(events.get(0).getTransitionType(), SubscriptionBaseTransitionType.CREATE);
Assert.assertEquals(events.get(0).getEffectiveDate(), subscription.getStartDate());
@@ -259,7 +259,7 @@ public class TestDefaultInternalBillingApi extends JunctionTestSuiteWithEmbedded
// Expected blocking duration:
// * 2013-08-07 to now [2013-08-07 to 2013-08-08 then 2013-08-08 to now]
- final List<BillingEvent> events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext));
+ final List<BillingEvent> events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, internalCallContext));
Assert.assertEquals(events.size(), 2);
Assert.assertEquals(events.get(0).getTransitionType(), SubscriptionBaseTransitionType.CREATE);
Assert.assertEquals(events.get(0).getEffectiveDate(), subscription.getStartDate());
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index f2d71f1..119e98c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>killbill-oss-parent</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.8.4</version>
+ <version>0.8.6</version>
</parent>
<artifactId>killbill</artifactId>
<version>0.11.14-SNAPSHOT</version>
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
index 390991d..0b803c3 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
@@ -26,16 +26,20 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
+import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.client.model.Account;
import org.killbill.billing.client.model.AuditLog;
import org.killbill.billing.client.model.Invoice;
+import org.killbill.billing.client.model.InvoiceDryRun;
import org.killbill.billing.client.model.InvoiceItem;
import org.killbill.billing.client.model.InvoicePayment;
import org.killbill.billing.client.model.InvoicePayments;
import org.killbill.billing.client.model.Invoices;
import org.killbill.billing.client.model.Payment;
import org.killbill.billing.client.model.PaymentMethod;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.payment.provider.ExternalPaymentProviderPlugin;
import org.killbill.billing.util.api.AuditLevel;
import org.testng.Assert;
@@ -89,7 +93,7 @@ public class TestInvoice extends TestJaxrsBase {
// Then create a dryRun Invoice
final DateTime futureDate = clock.getUTCNow().plusMonths(1).plusDays(3);
- killBillClient.createDryRunInvoice(accountJson.getAccountId(), futureDate, createdBy, reason, comment);
+ killBillClient.createDryRunInvoice(accountJson.getAccountId(), futureDate, null, createdBy, reason, comment);
// The one more time with no DryRun
killBillClient.createInvoice(accountJson.getAccountId(), futureDate, createdBy, reason, comment);
@@ -99,6 +103,21 @@ public class TestInvoice extends TestJaxrsBase {
assertEquals(newInvoiceList.size(), 3);
}
+
+ @Test(groups = "slow", description = "Can create a subscription in dryRun mode and get an invoice back")
+ public void testDryRunSubscriptionCreate() throws Exception {
+ final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ // "Assault-Rifle", BillingPeriod.ANNUAL, "rescue", BillingActionPolicy.IMMEDIATE,
+ final Account accountJson = createAccountWithDefaultPaymentMethod();
+ final InvoiceDryRun dryRunArg = new InvoiceDryRun(SubscriptionEventType.START_BILLING,
+ null, "Assault-Rifle", ProductCategory.BASE, BillingPeriod.ANNUAL, null, null, null, null, null);
+ final Invoice dryRunInvoice = killBillClient.createDryRunInvoice(accountJson.getAccountId(), initialDate, dryRunArg, createdBy, reason, comment);
+ assertEquals(dryRunInvoice.getItems().size(), 1);
+
+ }
+
@Test(groups = "slow", description = "Can retrieve invoice payments")
public void testInvoicePayments() throws Exception {
clock.setTime(new DateTime(2012, 4, 25, 0, 3, 42, 0));
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java b/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java
index 27bc7e6..4c194cc 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java
@@ -64,24 +64,26 @@ public class PlanAligner extends BaseAligner {
/**
* Returns the current and next phase for the subscription in creation
*
- * @param subscription the subscription in creation (only the start date and the bundle start date are looked at)
- * @param plan the current Plan
- * @param initialPhase the initialPhase on which we should create that subscription. can be null
- * @param priceList the priceList
- * @param requestedDate the requested date (only used to load the catalog)
- * @param effectiveDate the effective creation date (driven by the catalog policy, i.e. when the creation occurs)
+ * @param alignStartDate the subscription (align) startDate for the subscription
+ * @param bundleStartDate the bundle startDate used alignment
+ * @param plan the current Plan
+ * @param initialPhase the initialPhase on which we should create that subscription. can be null
+ * @param priceList the priceList
+ * @param requestedDate the requested date (only used to load the catalog)
+ * @param effectiveDate the effective creation date (driven by the catalog policy, i.e. when the creation occurs)
* @return the current and next phases
* @throws CatalogApiException for catalog errors
* @throws org.killbill.billing.subscription.api.user.SubscriptionBaseApiException for subscription errors
*/
- public TimedPhase[] getCurrentAndNextTimedPhaseOnCreate(final DefaultSubscriptionBase subscription,
+ public TimedPhase[] getCurrentAndNextTimedPhaseOnCreate(final DateTime alignStartDate,
+ final DateTime bundleStartDate,
final Plan plan,
- final PhaseType initialPhase,
+ @Nullable final PhaseType initialPhase,
final String priceList,
final DateTime requestedDate,
final DateTime effectiveDate) throws CatalogApiException, SubscriptionBaseApiException {
- final List<TimedPhase> timedPhases = getTimedPhaseOnCreate(subscription.getAlignStartDate(),
- subscription.getBundleStartDate(),
+ final List<TimedPhase> timedPhases = getTimedPhaseOnCreate(alignStartDate,
+ bundleStartDate,
plan,
initialPhase,
priceList,
@@ -187,7 +189,7 @@ public class PlanAligner extends BaseAligner {
private List<TimedPhase> getTimedPhaseOnCreate(final DateTime subscriptionStartDate,
final DateTime bundleStartDate,
final Plan plan,
- final PhaseType initialPhase,
+ @Nullable final PhaseType initialPhase,
final String priceList,
final DateTime requestedDate)
throws CatalogApiException, SubscriptionBaseApiException {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java
index cd0c710..af51ce0 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java
@@ -198,7 +198,8 @@ public class DefaultSubscriptionBaseMigrationApi extends SubscriptionApiBase imp
if (cur.getEventType() == EventType.PHASE) {
nextEventDate = nextEventDate != null && nextEventDate.compareTo(cur.getEventTime()) < 0 ? nextEventDate : cur.getEventTime();
- final PhaseEvent nextPhaseEvent = PhaseEventData.createNextPhaseEvent(cur.getPhase().getName(), defaultSubscriptionBase, now, cur.getEventTime());
+ final PhaseEvent nextPhaseEvent = PhaseEventData.createNextPhaseEvent(defaultSubscriptionBase.getId(), defaultSubscriptionBase.getActiveVersion(),
+ cur.getPhase().getName(), now, cur.getEventTime());
events.add(nextPhaseEvent);
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 2fdf041..7f67068 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
@@ -16,18 +16,24 @@
package org.killbill.billing.subscription.api;
-import org.joda.time.DateTime;
+import java.util.List;
+import java.util.UUID;
+import org.joda.time.DateTime;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanChangeResult;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.util.callcontext.CallContext;
-import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
public interface SubscriptionBaseApiService {
@@ -37,7 +43,7 @@ public interface SubscriptionBaseApiService {
throws SubscriptionBaseApiException;
@Deprecated
- public boolean recreatePlan(final DefaultSubscriptionBase subscription, final PlanPhaseSpecifier spec, final DateTime requestedDateWithMs, final CallContext context)
+ public boolean recreatePlan(DefaultSubscriptionBase subscription, PlanPhaseSpecifier spec, DateTime requestedDateWithMs, CallContext context)
throws SubscriptionBaseApiException;
public boolean cancel(DefaultSubscriptionBase subscription, CallContext context)
@@ -67,5 +73,26 @@ public interface SubscriptionBaseApiService {
String priceList, BillingActionPolicy policy, CallContext context)
throws SubscriptionBaseApiException;
- public int cancelAddOnsIfRequired(final DefaultSubscriptionBase baseSubscription, final DateTime effectiveDate, final InternalCallContext context);
-}
+ public int cancelAddOnsIfRequired(final Product baseProduct, final UUID bundleId, final DateTime effectiveDate, final CallContext context);
+
+ public PlanChangeResult getPlanChangeResult(final DefaultSubscriptionBase subscription, final String productName,
+ final BillingPeriod term, final String priceList, final DateTime effectiveDate) throws SubscriptionBaseApiException;
+
+ //
+ // Lower level APIs for dryRun functionality
+ //
+ public List<SubscriptionBaseEvent> getEventsOnCreation(UUID subscriptionId, DateTime alignStartDate, DateTime bundleStartDate, long activeVersion,
+ Plan plan, PhaseType initialPhase,
+ String realPriceList, DateTime requestedDate, DateTime effectiveDate, DateTime processedDate,
+ boolean reCreate, TenantContext context)
+ throws CatalogApiException, SubscriptionBaseApiException;
+
+ public List<SubscriptionBaseEvent> getEventsOnChangePlan(DefaultSubscriptionBase subscription, Plan newPlan,
+ String newPriceList, DateTime requestedDate, DateTime effectiveDate, DateTime processedDate,
+ boolean addCancellationAddOnForEventsIfRequired, TenantContext context)
+ throws CatalogApiException, SubscriptionBaseApiException;
+
+ public List<SubscriptionBaseEvent> getEventsOnCancelPlan(final DefaultSubscriptionBase subscription,
+ final DateTime requestedDate, final DateTime effectiveDate, final DateTime processedDate,
+ final boolean addCancellationAddOnForEventsIfRequired, final TenantContext context);
+}
\ No newline at end of file
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 29b1b20..9ba3261 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
@@ -16,6 +16,7 @@
package org.killbill.billing.subscription.api.svcs;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -25,29 +26,25 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
-import org.killbill.billing.util.cache.Cachable.CacheType;
-import org.killbill.billing.util.cache.CacheControllerDispatcher;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
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.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogService;
import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanChangeResult;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
-import org.killbill.clock.Clock;
-import org.killbill.clock.DefaultClock;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun.DryRunChangeReason;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.subscription.api.SubscriptionApiBase;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
@@ -64,10 +61,17 @@ import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
import org.killbill.billing.subscription.engine.addon.AddonUtils;
import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.dao.NonEntityDao;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
+import org.killbill.clock.Clock;
+import org.killbill.clock.DefaultClock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
@@ -111,7 +115,6 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
final Catalog catalog = catalogService.getFullCatalog();
final Plan plan = catalog.findPlan(spec.getProductName(), spec.getBillingPeriod(), realPriceList, requestedDate);
-
final PlanPhase phase = plan.getAllPhases()[0];
if (phase == null) {
throw new SubscriptionBaseError(String.format("No initial PlanPhase for Product %s, term %s and set %s does not exist in the catalog",
@@ -123,39 +126,8 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BUNDLE, bundleId);
}
- DateTime bundleStartDate = null;
final DefaultSubscriptionBase baseSubscription = (DefaultSubscriptionBase) dao.getBaseSubscription(bundleId, context);
- switch (plan.getProduct().getCategory()) {
- case BASE:
- if (baseSubscription != null) {
- if (baseSubscription.getState() == EntitlementState.ACTIVE) {
- throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundleId);
- }
- }
- bundleStartDate = requestedDate;
- break;
- case ADD_ON:
- if (baseSubscription == null) {
- throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BP, bundleId);
- }
- if (effectiveDate.isBefore(baseSubscription.getStartDate())) {
- throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_REQUESTED_DATE, effectiveDate.toString(), baseSubscription.getStartDate().toString());
- }
- addonUtils.checkAddonCreationRights(baseSubscription, plan);
- bundleStartDate = baseSubscription.getStartDate();
- break;
- case STANDALONE:
- if (baseSubscription != null) {
- throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundleId);
- }
- // Not really but we don't care, there is no alignment for STANDALONE subscriptions
- bundleStartDate = requestedDate;
- break;
- default:
- throw new SubscriptionBaseError(String.format("Can't create subscription of type %s",
- plan.getProduct().getCategory().toString()));
- }
-
+ final DateTime bundleStartDate = getBundleStartDateWithSanity(bundleId, baseSubscription, plan, requestedDate, effectiveDate);
final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT, controllerDispatcher.getCacheController(CacheType.OBJECT_ID));
return apiService.createPlan(new SubscriptionBuilder()
.setId(UUID.randomUUID())
@@ -240,7 +212,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
public static SubscriptionBaseBundle getActiveBundleForKeyNotException(final List<SubscriptionBaseBundle> existingBundles, final SubscriptionDao dao, final Clock clock, final InternalTenantContext context) {
for (SubscriptionBaseBundle cur : existingBundles) {
- final List<SubscriptionBase> subscriptions = dao.getSubscriptions(cur.getId(), context);
+ final List<SubscriptionBase> subscriptions = dao.getSubscriptions(cur.getId(), ImmutableList.<SubscriptionBaseEvent>of(), context);
for (SubscriptionBase s : subscriptions) {
if (s.getCategory() == ProductCategory.ADD_ON) {
continue;
@@ -255,9 +227,18 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
@Override
public List<SubscriptionBase> getSubscriptionsForBundle(UUID bundleId,
- InternalTenantContext context) {
- final List<SubscriptionBase> internalSubscriptions = dao.getSubscriptions(bundleId, context);
- return createSubscriptionsForApiUse(internalSubscriptions);
+ @Nullable final DryRunArguments dryRunArguments,
+ InternalTenantContext context) throws SubscriptionBaseApiException {
+
+ final List<SubscriptionBaseEvent> outputDryRunEvents = new ArrayList<SubscriptionBaseEvent>();
+ final List<SubscriptionBase> outputSubscriptions = new ArrayList<SubscriptionBase>();
+
+ populateDryRunEvents(bundleId, dryRunArguments, outputDryRunEvents, outputSubscriptions, context);
+ final List<SubscriptionBase> result = dao.getSubscriptions(bundleId, outputDryRunEvents, context);
+ if (result != null && !result.isEmpty()) {
+ outputSubscriptions.addAll(result);
+ }
+ return createSubscriptionsForApiUse(outputSubscriptions);
}
@Override
@@ -328,23 +309,6 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
}
@Override
- public DateTime getNextBillingDate(final UUID accountId, final InternalTenantContext context) {
- final List<SubscriptionBaseBundle> bundles = getBundlesForAccount(accountId, context);
- DateTime result = null;
- for (final SubscriptionBaseBundle bundle : bundles) {
- final List<SubscriptionBase> subscriptions = getSubscriptionsForBundle(bundle.getId(), context);
- for (final SubscriptionBase subscription : subscriptions) {
- final DateTime chargedThruDate = subscription.getChargedThroughDate();
- if (result == null ||
- (chargedThruDate != null && chargedThruDate.isBefore(result))) {
- result = subscription.getChargedThroughDate();
- }
- }
- }
- return result;
- }
-
- @Override
public List<EntitlementAOStatusDryRun> getDryRunChangePlanStatus(final UUID subscriptionId, @Nullable final String baseProductName, final DateTime requestedDate, final InternalTenantContext context) throws SubscriptionBaseApiException {
final SubscriptionBase subscription = dao.getSubscriptionFromId(subscriptionId, context);
if (subscription == null) {
@@ -356,7 +320,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
final List<EntitlementAOStatusDryRun> result = new LinkedList<EntitlementAOStatusDryRun>();
- final List<SubscriptionBase> bundleSubscriptions = dao.getSubscriptions(subscription.getBundleId(), context);
+ final List<SubscriptionBase> bundleSubscriptions = dao.getSubscriptions(subscription.getBundleId(), ImmutableList.<SubscriptionBaseEvent>of(), context);
for (final SubscriptionBase cur : bundleSubscriptions) {
if (cur.getId().equals(subscriptionId)) {
continue;
@@ -391,6 +355,128 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
dao.updateBundleExternalKey(bundleId, newExternalKey, context);
}
+ private void populateDryRunEvents(@Nullable final UUID bundleId,
+ @Nullable final DryRunArguments dryRunArguments,
+ final List<SubscriptionBaseEvent> outputDryRunEvents,
+ final List<SubscriptionBase> outputSubscriptions,
+ InternalTenantContext context) throws SubscriptionBaseApiException {
+ if (dryRunArguments == null || dryRunArguments.getAction() == null) {
+ return;
+ }
+
+ final DateTime utcNow = clock.getUTCNow();
+ List<SubscriptionBaseEvent> dryRunEvents = null;
+ try {
+ final PlanPhaseSpecifier inputSpec = dryRunArguments.getPlanPhaseSpecifier();
+ final String realPriceList = (inputSpec != null && inputSpec.getPriceListName() != null) ? inputSpec.getPriceListName() : PriceListSet.DEFAULT_PRICELIST_NAME;
+ final Catalog catalog = catalogService.getFullCatalog();
+ final Plan plan = (inputSpec != null && inputSpec.getProductName() != null && inputSpec.getBillingPeriod() != null) ?
+ catalog.findPlan(inputSpec.getProductName(), inputSpec.getBillingPeriod(), realPriceList, utcNow) : null;
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT, controllerDispatcher.getCacheController(CacheType.OBJECT_ID));
+
+ if (dryRunArguments != null) {
+ switch (dryRunArguments.getAction()) {
+ case START_BILLING:
+
+ final DefaultSubscriptionBase baseSubscription = (DefaultSubscriptionBase) dao.getBaseSubscription(bundleId, context);
+ final DateTime startEffectiveDate = dryRunArguments.getEffectiveDate() != null ? dryRunArguments.getEffectiveDate() : utcNow;
+ final DateTime bundleStartDate = getBundleStartDateWithSanity(bundleId, baseSubscription, plan, startEffectiveDate, startEffectiveDate);
+ final UUID subscriptionId = UUID.randomUUID();
+ dryRunEvents = apiService.getEventsOnCreation(subscriptionId, startEffectiveDate, bundleStartDate, 1L, plan, inputSpec.getPhaseType(), realPriceList,
+ utcNow, startEffectiveDate, utcNow, false, context.toTenantContext(tenantId));
+ final SubscriptionBuilder builder = new SubscriptionBuilder()
+ .setId(subscriptionId)
+ .setBundleId(bundleId)
+ .setCategory(plan.getProduct().getCategory())
+ .setBundleStartDate(bundleStartDate)
+ .setAlignStartDate(startEffectiveDate);
+ final DefaultSubscriptionBase newSubscription = new DefaultSubscriptionBase(builder, apiService, clock);
+ newSubscription.rebuildTransitions(dryRunEvents, catalog);
+ outputSubscriptions.add(newSubscription);
+ break;
+
+ case CHANGE:
+ final DefaultSubscriptionBase subscriptionForChange = (DefaultSubscriptionBase) dao.getSubscriptionFromId(dryRunArguments.getSubscriptionId(), context);
+ DateTime changeEffectiveDate = dryRunArguments.getEffectiveDate();
+ if (changeEffectiveDate == null) {
+ BillingActionPolicy policy = dryRunArguments.getBillingActionPolicy();
+ if (policy == null) {
+ final PlanChangeResult planChangeResult = apiService.getPlanChangeResult(subscriptionForChange,
+ dryRunArguments.getPlanPhaseSpecifier().getProductName(),
+ dryRunArguments.getPlanPhaseSpecifier().getBillingPeriod(),
+ dryRunArguments.getPlanPhaseSpecifier().getPriceListName(), utcNow);
+ policy = planChangeResult.getPolicy();
+ }
+ changeEffectiveDate = subscriptionForChange.getPlanChangeEffectiveDate(policy);
+ }
+ dryRunEvents = apiService.getEventsOnChangePlan(subscriptionForChange, plan, realPriceList, utcNow, changeEffectiveDate, utcNow, true, context.toTenantContext(tenantId));
+ break;
+
+ case STOP_BILLING:
+ final DefaultSubscriptionBase subscriptionForCancellation = (DefaultSubscriptionBase) dao.getSubscriptionFromId(dryRunArguments.getSubscriptionId(), context);
+ DateTime cancelEffectiveDate = dryRunArguments.getEffectiveDate();
+ if (dryRunArguments.getEffectiveDate() == null) {
+ BillingActionPolicy policy = dryRunArguments.getBillingActionPolicy();
+ if (policy == null) {
+
+ final Plan currentPlan = subscriptionForCancellation.getCurrentPlan();
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(currentPlan.getProduct().getName(),
+ currentPlan.getProduct().getCategory(),
+ subscriptionForCancellation.getCurrentPlan().getRecurringBillingPeriod(),
+ subscriptionForCancellation.getCurrentPriceList().getName(),
+ subscriptionForCancellation.getCurrentPhase().getPhaseType());
+ policy = catalogService.getFullCatalog().planCancelPolicy(spec, utcNow);
+ }
+ cancelEffectiveDate = subscriptionForCancellation.getPlanChangeEffectiveDate(policy);
+ }
+ dryRunEvents = apiService.getEventsOnCancelPlan(subscriptionForCancellation, utcNow, cancelEffectiveDate, utcNow, true, context.toTenantContext(tenantId));
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unexpected dryRunArguments action " + dryRunArguments.getAction());
+ }
+ }
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
+ }
+ if (dryRunEvents != null && !dryRunEvents.isEmpty()) {
+ outputDryRunEvents.addAll(dryRunEvents);
+ }
+ }
+
+ private DateTime getBundleStartDateWithSanity(final UUID bundleId, @Nullable final DefaultSubscriptionBase baseSubscription, final Plan plan,
+ final DateTime requestedDate, final DateTime effectiveDate) throws SubscriptionBaseApiException, CatalogApiException {
+ switch (plan.getProduct().getCategory()) {
+ case BASE:
+ if (baseSubscription != null &&
+ baseSubscription.getState() == EntitlementState.ACTIVE) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundleId);
+ }
+ return requestedDate;
+
+ case ADD_ON:
+ if (baseSubscription == null) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BP, bundleId);
+ }
+ if (effectiveDate.isBefore(baseSubscription.getStartDate())) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_REQUESTED_DATE, effectiveDate.toString(), baseSubscription.getStartDate().toString());
+ }
+ addonUtils.checkAddonCreationRights(baseSubscription, plan);
+ return baseSubscription.getStartDate();
+
+ case STANDALONE:
+ if (baseSubscription != null) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundleId);
+ }
+ // Not really but we don't care, there is no alignment for STANDALONE subscriptions
+ return requestedDate;
+
+ default:
+ throw new SubscriptionBaseError(String.format("Can't create subscription of type %s",
+ plan.getProduct().getCategory().toString()));
+ }
+ }
+
private List<EffectiveSubscriptionInternalEvent> convertEffectiveSubscriptionInternalEventFromSubscriptionTransitions(final SubscriptionBase subscription,
final InternalTenantContext context, final List<SubscriptionBaseTransition> transitions) {
return ImmutableList.<EffectiveSubscriptionInternalEvent>copyOf(Collections2.transform(transitions, new Function<SubscriptionBaseTransition, EffectiveSubscriptionInternalEvent>() {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java
index 44119f5..01b74ae 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java
@@ -57,6 +57,7 @@ import org.killbill.clock.Clock;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import com.google.inject.name.Named;
@@ -115,7 +116,9 @@ public class DefaultSubscriptionBaseTimelineApi extends SubscriptionApiBase impl
if (bundle == null) {
throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_UNKNOWN_BUNDLE, descBundle);
}
- final List<SubscriptionDataRepair> subscriptions = convertToSubscriptionsDataRepair(dao.getSubscriptions(bundle.getId(), internalCallContextFactory.createInternalTenantContext(context)));
+ final List<SubscriptionDataRepair> subscriptions = convertToSubscriptionsDataRepair(dao.getSubscriptions(bundle.getId(),
+ ImmutableList.<SubscriptionBaseEvent>of(),
+ internalCallContextFactory.createInternalTenantContext(context)));
if (subscriptions.size() == 0) {
throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_NO_ACTIVE_SUBSCRIPTIONS, bundle.getId());
}
@@ -149,7 +152,7 @@ public class DefaultSubscriptionBaseTimelineApi extends SubscriptionApiBase impl
}
// Subscriptions are ordered with BASE subscription first-- if exists
- final List<SubscriptionDataRepair> subscriptions = convertToSubscriptionsDataRepair(dao.getSubscriptions(input.getId(), tenantContext));
+ final List<SubscriptionDataRepair> subscriptions = convertToSubscriptionsDataRepair(dao.getSubscriptions(input.getId(), ImmutableList.<SubscriptionBaseEvent>of(), tenantContext));
if (subscriptions.size() == 0) {
throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_NO_ACTIVE_SUBSCRIPTIONS, input.getId());
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionApiService.java
index 2d1da3d..6a0efd4 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionApiService.java
@@ -16,10 +16,11 @@
package org.killbill.billing.subscription.api.timeline;
-import org.joda.time.DateTime;
+import java.util.UUID;
+import org.joda.time.DateTime;
import org.killbill.billing.catalog.api.CatalogService;
-import org.killbill.clock.Clock;
+import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.subscription.alignment.PlanAligner;
import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
@@ -27,8 +28,9 @@ import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseApiServ
import org.killbill.billing.subscription.engine.addon.AddonUtils;
import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
import org.killbill.billing.subscription.glue.DefaultSubscriptionModule;
-import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.Clock;
import com.google.inject.Inject;
import com.google.inject.name.Named;
@@ -47,7 +49,7 @@ public class RepairSubscriptionApiService extends DefaultSubscriptionBaseApiServ
// Nothing to do for repair as we pass all the repair events in the stream
@Override
- public int cancelAddOnsIfRequired(final DefaultSubscriptionBase baseSubscription, final DateTime effectiveDate, final InternalCallContext context) {
+ public int cancelAddOnsIfRequired(final Product baseProduct, final UUID bundleId, final DateTime effectiveDate, final CallContext context) {
return 0;
}
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java
index eb47278..c7d0a18 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java
@@ -118,7 +118,7 @@ public class DefaultSubscriptionBaseTransferApi extends SubscriptionApiBase impl
case PHASE:
newEvent = firstEvent ? new ApiEventTransfer(apiBuilder) :
- PhaseEventData.createNextPhaseEvent(currentPhase.getName(), subscription, clock.getUTCNow(), effectiveDate);
+ PhaseEventData.createNextPhaseEvent(subscription.getId(), subscription.getActiveVersion(), currentPhase.getName(), clock.getUTCNow(), effectiveDate);
break;
// Ignore these events except if it's the first event for the new subscription
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 a75c96a..1d529d5 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
@@ -23,10 +23,10 @@ import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
-
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
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.BillingPeriod;
import org.killbill.billing.catalog.api.CatalogApiException;
@@ -41,8 +41,6 @@ import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.catalog.api.ProductCategory;
-import org.killbill.clock.Clock;
-import org.killbill.clock.DefaultClock;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.subscription.alignment.PlanAligner;
import org.killbill.billing.subscription.alignment.TimedPhase;
@@ -63,7 +61,11 @@ import org.killbill.billing.subscription.events.user.ApiEventUncancel;
import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.clock.DefaultClock;
+import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiService {
@@ -134,29 +136,8 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
try {
- final TimedPhase[] curAndNextPhases = planAligner.getCurrentAndNextTimedPhaseOnCreate(subscription, plan, initialPhase, realPriceList, requestedDate, effectiveDate);
-
- final ApiEventBuilder createBuilder = new ApiEventBuilder()
- .setSubscriptionId(subscription.getId())
- .setEventPlan(plan.getName())
- .setEventPlanPhase(curAndNextPhases[0].getPhase().getName())
- .setEventPriceList(realPriceList)
- .setActiveVersion(subscription.getActiveVersion())
- .setProcessedDate(processedDate)
- .setEffectiveDate(effectiveDate)
- .setRequestedDate(requestedDate)
- .setFromDisk(true);
- final ApiEvent creationEvent = (reCreate) ? new ApiEventReCreate(createBuilder) : new ApiEventCreate(createBuilder);
-
- final TimedPhase nextTimedPhase = curAndNextPhases[1];
- final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
- PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, processedDate, nextTimedPhase.getStartPhase()) :
- null;
- final List<SubscriptionBaseEvent> events = new ArrayList<SubscriptionBaseEvent>();
- events.add(creationEvent);
- if (nextPhaseEvent != null) {
- events.add(nextPhaseEvent);
- }
+ final List<SubscriptionBaseEvent> events = getEventsOnCreation(subscription.getId(), subscription.getAlignStartDate(), subscription.getBundleStartDate(), subscription.getActiveVersion(),
+ plan, initialPhase, realPriceList, requestedDate, effectiveDate, processedDate, reCreate, context);
if (reCreate) {
dao.recreateSubscription(subscription, events, internalCallContext);
} else {
@@ -168,6 +149,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
}
}
+
@Override
public boolean cancel(final DefaultSubscriptionBase subscription, final CallContext context) throws SubscriptionBaseApiException {
@@ -220,27 +202,23 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
private boolean doCancelPlan(final DefaultSubscriptionBase subscription, final DateTime now, final DateTime effectiveDate, final CallContext context) throws SubscriptionBaseApiException {
validateEffectiveDate(subscription, effectiveDate);
- final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
- .setSubscriptionId(subscription.getId())
- .setActiveVersion(subscription.getActiveVersion())
- .setProcessedDate(now)
- .setEffectiveDate(effectiveDate)
- .setRequestedDate(now)
- .setFromDisk(true));
-
+ final List<SubscriptionBaseEvent> cancelEvents = getEventsOnCancelPlan(subscription, now, effectiveDate, now, false, context);
final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
- dao.cancelSubscription(subscription, cancelEvent, internalCallContext, 0);
+ // cancelEvents will contain only one item
+ dao.cancelSubscription(subscription, cancelEvents.get(0), internalCallContext, 0);
subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), catalogService.getFullCatalog());
if (subscription.getCategory() == ProductCategory.BASE) {
- cancelAddOnsIfRequired(subscription, effectiveDate, internalCallContext);
+ final Product baseProduct = (subscription.getState() == EntitlementState.CANCELLED) ? null : subscription.getCurrentPlan().getProduct();
+ cancelAddOnsIfRequired(baseProduct, subscription.getBundleId(), effectiveDate, context);
}
final boolean isImmediate = subscription.getState() == EntitlementState.CANCELLED;
return isImmediate;
}
- @Override
+
+ @Override
public boolean uncancel(final DefaultSubscriptionBase subscription, final CallContext context) throws SubscriptionBaseApiException {
if (!subscription.isSubscriptionFutureCancelled()) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_UNCANCEL_BAD_STATE, subscription.getId().toString());
@@ -260,7 +238,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
final TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription, now, now);
final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
- PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
+ PhaseEventData.createNextPhaseEvent(subscription.getId(), subscription.getActiveVersion(), nextTimedPhase.getPhase().getName(), now, nextTimedPhase.getStartPhase()) :
null;
if (nextPhaseEvent != null) {
uncancelEvents.add(nextPhaseEvent);
@@ -291,6 +269,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
}
}
+
@Override
public DateTime changePlanWithRequestedDate(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
final String priceList, final DateTime requestedDateWithMs, final CallContext context) throws SubscriptionBaseApiException {
@@ -323,8 +302,9 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
}
}
- private PlanChangeResult getPlanChangeResult(final DefaultSubscriptionBase subscription, final String productName,
- final BillingPeriod term, final String priceList, final DateTime effectiveDate) throws SubscriptionBaseApiException {
+ @Override
+ public PlanChangeResult getPlanChangeResult(final DefaultSubscriptionBase subscription, final String productName,
+ final BillingPeriod term, final String priceList, final DateTime effectiveDate) throws SubscriptionBaseApiException {
final PlanChangeResult planChangeResult;
try {
final Product destProduct = catalogService.getFullCatalog().findProduct(productName, effectiveDate);
@@ -358,7 +338,57 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
final Plan newPlan = catalogService.getFullCatalog().findPlan(newProductName, newBillingPeriod, newPriceList, effectiveDate, subscription.getStartDate());
- final TimedPhase currentTimedPhase = planAligner.getCurrentTimedPhaseOnChange(subscription, newPlan, newPriceList, now, effectiveDate);
+ final List<SubscriptionBaseEvent> changeEvents = getEventsOnChangePlan(subscription, newPlan, newPriceList, now, effectiveDate, now, false, context);
+ final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
+ dao.changePlan(subscription, changeEvents, internalCallContext);
+ subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), catalogService.getFullCatalog());
+
+ if (subscription.getCategory() == ProductCategory.BASE) {
+ final Product baseProduct = (subscription.getState() == EntitlementState.CANCELLED) ? null : subscription.getCurrentPlan().getProduct();
+ cancelAddOnsIfRequired(baseProduct, subscription.getBundleId(), effectiveDate, context);
+ }
+ return effectiveDate;
+ }
+
+ @Override
+ public List<SubscriptionBaseEvent> getEventsOnCreation(final UUID subscriptionId, final DateTime alignStartDate, final DateTime bundleStartDate, final long activeVersion,
+ final Plan plan, final PhaseType initialPhase,
+ final String realPriceList, final DateTime requestedDate, final DateTime effectiveDate, final DateTime processedDate,
+ final boolean reCreate, final TenantContext context) throws CatalogApiException, SubscriptionBaseApiException {
+ final TimedPhase[] curAndNextPhases = planAligner.getCurrentAndNextTimedPhaseOnCreate(alignStartDate, bundleStartDate, plan, initialPhase,
+ realPriceList, requestedDate, effectiveDate);
+
+ final ApiEventBuilder createBuilder = new ApiEventBuilder()
+ .setSubscriptionId(subscriptionId)
+ .setEventPlan(plan.getName())
+ .setEventPlanPhase(curAndNextPhases[0].getPhase().getName())
+ .setEventPriceList(realPriceList)
+ .setActiveVersion(activeVersion)
+ .setProcessedDate(processedDate)
+ .setEffectiveDate(effectiveDate)
+ .setRequestedDate(requestedDate)
+ .setFromDisk(true);
+ final ApiEvent creationEvent = (reCreate) ? new ApiEventReCreate(createBuilder) : new ApiEventCreate(createBuilder);
+
+ final TimedPhase nextTimedPhase = curAndNextPhases[1];
+ final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
+ PhaseEventData.createNextPhaseEvent(subscriptionId, activeVersion, nextTimedPhase.getPhase().getName(), processedDate, nextTimedPhase.getStartPhase()) :
+ null;
+ final List<SubscriptionBaseEvent> events = new ArrayList<SubscriptionBaseEvent>();
+ events.add(creationEvent);
+ if (nextPhaseEvent != null) {
+ events.add(nextPhaseEvent);
+ }
+ return events;
+ }
+
+
+ @Override
+ public List<SubscriptionBaseEvent> getEventsOnChangePlan(final DefaultSubscriptionBase subscription, final Plan newPlan,
+ final String newPriceList, final DateTime requestedDate, final DateTime effectiveDate, final DateTime processedDate,
+ final boolean addCancellationAddOnForEventsIfRequired, final TenantContext context) throws CatalogApiException, SubscriptionBaseApiException {
+
+ final TimedPhase currentTimedPhase = planAligner.getCurrentTimedPhaseOnChange(subscription, newPlan, newPriceList, requestedDate, effectiveDate);
final SubscriptionBaseEvent changeEvent = new ApiEventChange(new ApiEventBuilder()
.setSubscriptionId(subscription.getId())
@@ -366,14 +396,15 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
.setEventPlanPhase(currentTimedPhase.getPhase().getName())
.setEventPriceList(newPriceList)
.setActiveVersion(subscription.getActiveVersion())
- .setProcessedDate(now)
+ .setProcessedDate(processedDate)
.setEffectiveDate(effectiveDate)
- .setRequestedDate(now)
+ .setRequestedDate(requestedDate)
.setFromDisk(true));
- final TimedPhase nextTimedPhase = planAligner.getNextTimedPhaseOnChange(subscription, newPlan, newPriceList, now, effectiveDate);
+ final TimedPhase nextTimedPhase = planAligner.getNextTimedPhaseOnChange(subscription, newPlan, newPriceList, processedDate, effectiveDate);
final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
- PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
+ PhaseEventData.createNextPhaseEvent(subscription.getId(), subscription.getActiveVersion(),
+ nextTimedPhase.getPhase().getName(), processedDate, nextTimedPhase.getStartPhase()) :
null;
final List<SubscriptionBaseEvent> changeEvents = new ArrayList<SubscriptionBaseEvent>();
@@ -383,20 +414,36 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
changeEvents.add(nextPhaseEvent);
}
- final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
- dao.changePlan(subscription, changeEvents, internalCallContext);
- subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), catalogService.getFullCatalog());
-
- if (subscription.getCategory() == ProductCategory.BASE) {
- cancelAddOnsIfRequired(subscription, effectiveDate, internalCallContext);
+ if (subscription.getCategory() == ProductCategory.BASE && addCancellationAddOnForEventsIfRequired) {
+ final Product currentBaseProduct = changeEvent.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0 ? newPlan.getProduct() : subscription.getCurrentPlan().getProduct();
+ addCancellationAddOnForEventsIfRequired(changeEvents, currentBaseProduct, subscription.getBundleId(), requestedDate, effectiveDate, processedDate, context);
}
+ return changeEvents;
+ }
- final boolean isChangeImmediate = subscription.getCurrentPlan().getProduct().getName().equals(newProductName) &&
- subscription.getCurrentPlan().getRecurringBillingPeriod() == newBillingPeriod;
- return effectiveDate;
+ @Override
+ public List<SubscriptionBaseEvent> getEventsOnCancelPlan(final DefaultSubscriptionBase subscription,
+ final DateTime requestedDate, final DateTime effectiveDate, final DateTime processedDate,
+ final boolean addCancellationAddOnForEventsIfRequired, final TenantContext context) {
+
+ final List<SubscriptionBaseEvent> cancelEvents = new ArrayList<SubscriptionBaseEvent>();
+ final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
+ .setSubscriptionId(subscription.getId())
+ .setActiveVersion(subscription.getActiveVersion())
+ .setProcessedDate(processedDate)
+ .setEffectiveDate(effectiveDate)
+ .setRequestedDate(requestedDate)
+ .setFromDisk(true));
+ cancelEvents.add(cancelEvent);
+ if (subscription.getCategory() == ProductCategory.BASE && addCancellationAddOnForEventsIfRequired) {
+ final Product currentBaseProduct = cancelEvent.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0 ? null : subscription.getCurrentPlan().getProduct();
+ addCancellationAddOnForEventsIfRequired(cancelEvents, currentBaseProduct, subscription.getBundleId(), requestedDate, effectiveDate, processedDate, context);
+ }
+ return cancelEvents;
}
- public int cancelAddOnsIfRequired(final DefaultSubscriptionBase baseSubscription, final DateTime effectiveDate, final InternalCallContext context) {
+
+ public int cancelAddOnsIfRequired(final Product baseProduct, final UUID bundleId, final DateTime effectiveDate, final CallContext context) {
// If cancellation/change occur in the future, there is nothing to do
final DateTime now = clock.getUTCNow();
@@ -404,12 +451,22 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
return 0;
}
- final Product baseProduct = (baseSubscription.getState() == EntitlementState.CANCELLED) ? null : baseSubscription.getCurrentPlan().getProduct();
+ final List<SubscriptionBaseEvent> cancelEvents = new LinkedList<SubscriptionBaseEvent>();
+ final List<DefaultSubscriptionBase> subscriptionsToBeCancelled = addCancellationAddOnForEventsIfRequired(cancelEvents, baseProduct, bundleId, now, effectiveDate, now, context);
+ if (!subscriptionsToBeCancelled.isEmpty()) {
+ final InternalCallContext internalCallContext = createCallContextFromBundleId(bundleId, context);
+ dao.cancelSubscriptions(subscriptionsToBeCancelled, cancelEvents, internalCallContext);
+ }
+ return subscriptionsToBeCancelled.size();
+ }
- final List<SubscriptionBase> subscriptions = dao.getSubscriptions(baseSubscription.getBundleId(), context);
+ private List<DefaultSubscriptionBase> addCancellationAddOnForEventsIfRequired(final List<SubscriptionBaseEvent> events, final Product baseProduct, final UUID bundleId,
+ final DateTime requestedDate, final DateTime effectiveDate, final DateTime processedDate, final TenantContext context) {
- final List<DefaultSubscriptionBase> subscriptionsToBeCancelled = new LinkedList<DefaultSubscriptionBase>();
- final List<SubscriptionBaseEvent> cancelEvents = new LinkedList<SubscriptionBaseEvent>();
+ final List<DefaultSubscriptionBase> subscriptionsToBeCancelled = new ArrayList<DefaultSubscriptionBase>();
+
+ final InternalTenantContext internalCallContext = createTenantContextFromBundleId(bundleId, context);
+ final List<SubscriptionBase> subscriptions = dao.getSubscriptions(bundleId, ImmutableList.<SubscriptionBaseEvent>of(), internalCallContext);
for (final SubscriptionBase subscription : subscriptions) {
final DefaultSubscriptionBase cur = (DefaultSubscriptionBase) subscription;
@@ -428,17 +485,15 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
.setSubscriptionId(cur.getId())
.setActiveVersion(cur.getActiveVersion())
- .setProcessedDate(now)
+ .setProcessedDate(processedDate)
.setEffectiveDate(effectiveDate)
- .setRequestedDate(now)
+ .setRequestedDate(requestedDate)
.setFromDisk(true));
subscriptionsToBeCancelled.add(cur);
- cancelEvents.add(cancelEvent);
+ events.add(cancelEvent);
}
}
-
- dao.cancelSubscriptions(subscriptionsToBeCancelled, cancelEvents, context);
- return subscriptionsToBeCancelled.size();
+ return subscriptionsToBeCancelled;
}
private void validateEffectiveDate(final DefaultSubscriptionBase subscription, final DateTime effectiveDate) throws SubscriptionBaseApiException {
@@ -462,4 +517,8 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
private InternalCallContext createCallContextFromBundleId(final UUID bundleId, final CallContext context) {
return internalCallContextFactory.createInternalCallContext(bundleId, ObjectType.BUNDLE, context);
}
+
+ private InternalTenantContext createTenantContextFromBundleId(final UUID bundleId, final TenantContext context) {
+ return internalCallContextFactory.createInternalTenantContext(bundleId, ObjectType.BUNDLE, context);
+ }
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
index c38610b..8178614 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
@@ -21,8 +21,11 @@ package org.killbill.billing.subscription.engine.core;
import java.util.UUID;
import org.joda.time.DateTime;
+import org.killbill.billing.ObjectType;
import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
import org.killbill.billing.platform.api.LifecycleHandlerType;
import org.killbill.billing.platform.api.LifecycleHandlerType.LifecycleLevel;
@@ -41,9 +44,13 @@ import org.killbill.billing.subscription.events.phase.PhaseEvent;
import org.killbill.billing.subscription.events.phase.PhaseEventData;
import org.killbill.billing.subscription.events.user.ApiEvent;
import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.CallOrigin;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.billing.util.dao.NonEntityDao;
import org.killbill.bus.api.PersistentBus;
import org.killbill.bus.api.PersistentBus.EventBusException;
import org.killbill.clock.Clock;
@@ -68,27 +75,31 @@ public class DefaultSubscriptionBaseService implements EventListener, Subscripti
private final Clock clock;
private final SubscriptionDao dao;
private final PlanAligner planAligner;
- private final AddonUtils addonUtils;
private final PersistentBus eventBus;
private final NotificationQueueService notificationQueueService;
private final InternalCallContextFactory internalCallContextFactory;
private NotificationQueue subscriptionEventQueue;
private final SubscriptionBaseApiService apiService;
+ private final NonEntityDao nonEntityDao;
+ private final CacheControllerDispatcher controllerDispatcher;
@Inject
public DefaultSubscriptionBaseService(final Clock clock, final SubscriptionDao dao, final PlanAligner planAligner,
- final AddonUtils addonUtils, final PersistentBus eventBus,
+ final PersistentBus eventBus,
final NotificationQueueService notificationQueueService,
final InternalCallContextFactory internalCallContextFactory,
- final SubscriptionBaseApiService apiService) {
+ final SubscriptionBaseApiService apiService,
+ final NonEntityDao nonEntityDao,
+ final CacheControllerDispatcher controllerDispatcher) {
this.clock = clock;
this.dao = dao;
this.planAligner = planAligner;
- this.addonUtils = addonUtils;
this.eventBus = eventBus;
this.notificationQueueService = notificationQueueService;
this.internalCallContextFactory = internalCallContextFactory;
this.apiService = apiService;
+ this.nonEntityDao = nonEntityDao;
+ this.controllerDispatcher = controllerDispatcher;
}
@Override
@@ -164,7 +175,8 @@ public class DefaultSubscriptionBaseService implements EventListener, Subscripti
if (event.getType() == EventType.PHASE) {
onPhaseEvent(subscription, context);
} else if (event.getType() == EventType.API_USER && subscription.getCategory() == ProductCategory.BASE) {
- theRealSeqId = onBasePlanEvent(subscription, (ApiEvent) event, context);
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT, controllerDispatcher.getCacheController(CacheType.OBJECT_ID));
+ theRealSeqId = onBasePlanEvent(subscription, (ApiEvent) event, context.toCallContext(tenantId));
}
try {
@@ -183,7 +195,8 @@ public class DefaultSubscriptionBaseService implements EventListener, Subscripti
final DateTime now = clock.getUTCNow();
final TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription, now, now);
final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
- PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
+ PhaseEventData.createNextPhaseEvent(subscription.getId(), subscription.getActiveVersion(),
+ nextTimedPhase.getPhase().getName(), now, nextTimedPhase.getStartPhase()) :
null;
if (nextPhaseEvent != null) {
dao.createNextPhaseEvent(subscription, nextPhaseEvent, context);
@@ -193,7 +206,8 @@ public class DefaultSubscriptionBaseService implements EventListener, Subscripti
}
}
- private int onBasePlanEvent(final DefaultSubscriptionBase baseSubscription, final ApiEvent event, final InternalCallContext context) {
- return apiService.cancelAddOnsIfRequired(baseSubscription, event.getEffectiveDate(), context);
+ private int onBasePlanEvent(final DefaultSubscriptionBase baseSubscription, final ApiEvent event, final CallContext context) {
+ final Product baseProduct = (baseSubscription.getState() == EntitlementState.CANCELLED) ? null : baseSubscription.getCurrentPlan().getProduct();
+ return apiService.cancelAddOnsIfRequired(baseProduct, baseSubscription.getBundleId(), event.getEffectiveDate(), context);
}
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
index ab83318..7aa2fd9 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
@@ -305,8 +305,8 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
}
@Override
- public List<SubscriptionBase> getSubscriptions(final UUID bundleId, final InternalTenantContext context) {
- return buildBundleSubscriptions(getSubscriptionFromBundleId(bundleId, context), null, context);
+ public List<SubscriptionBase> getSubscriptions(final UUID bundleId, final List<SubscriptionBaseEvent> dryRunEvents, final InternalTenantContext context) {
+ return buildBundleSubscriptions(getSubscriptionFromBundleId(bundleId, context), null, dryRunEvents, context);
}
private List<SubscriptionBase> getSubscriptionFromBundleId(final UUID bundleId, final InternalTenantContext context) {
@@ -353,7 +353,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
eventsForSubscriptions.putAll(cur.getId(), ImmutableList.copyOf(events));
}
- result.put(bundleId, buildBundleSubscriptions(subscriptionsForBundle, eventsForSubscriptions, context));
+ result.put(bundleId, buildBundleSubscriptions(subscriptionsForBundle, eventsForSubscriptions, null, context));
}
return result;
}
@@ -830,7 +830,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
bundleInput.add(input);
}
- final List<SubscriptionBase> reloadedSubscriptions = buildBundleSubscriptions(bundleInput, null, context);
+ final List<SubscriptionBase> reloadedSubscriptions = buildBundleSubscriptions(bundleInput, null, null, context);
for (final SubscriptionBase cur : reloadedSubscriptions) {
if (cur.getId().equals(input.getId())) {
return cur;
@@ -840,7 +840,9 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
throw new SubscriptionBaseError("Unexpected code path in buildSubscription");
}
- private List<SubscriptionBase> buildBundleSubscriptions(final List<SubscriptionBase> input, @Nullable final Multimap<UUID, SubscriptionBaseEvent> eventsForSubscription, final InternalTenantContext context) {
+
+ private List<SubscriptionBase> buildBundleSubscriptions(final List<SubscriptionBase> input, @Nullable final Multimap<UUID, SubscriptionBaseEvent> eventsForSubscription,
+ @Nullable List<SubscriptionBaseEvent> dryRunEvents, final InternalTenantContext context) {
if (input == null || input.size() == 0) {
return Collections.emptyList();
}
@@ -866,6 +868,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
final List<SubscriptionBaseEvent> events = eventsForSubscription != null ?
(List<SubscriptionBaseEvent>) eventsForSubscription.get(cur.getId()) :
getEventsForSubscription(cur.getId(), context);
+ mergeDryRunEvents(cur.getId(), events, dryRunEvents);
SubscriptionBase reloaded = createSubscriptionForInternalUse(cur, events);
@@ -919,6 +922,26 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
return result;
}
+ private void mergeDryRunEvents(final UUID subscriptionId, final List<SubscriptionBaseEvent> events, @Nullable List<SubscriptionBaseEvent> dryRunEvents) {
+ if (dryRunEvents == null || dryRunEvents.isEmpty()) {
+ return;
+ }
+ for (SubscriptionBaseEvent curDryRun : dryRunEvents) {
+ if (curDryRun.getSubscriptionId() != null && curDryRun.getSubscriptionId().equals(subscriptionId)) {
+
+ //boolean inserted = false;
+ final Iterator<SubscriptionBaseEvent> it = events.iterator();
+ while (it.hasNext()) {
+ final SubscriptionBaseEvent event = it.next();
+ if (event.getEffectiveDate().isAfter(curDryRun.getEffectiveDate())) {
+ it.remove();
+ }
+ }
+ events.add(curDryRun);
+ }
+ }
+ }
+
@Override
public void migrate(final UUID accountId, final AccountMigrationData accountData, final InternalCallContext context) {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/RepairSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/RepairSubscriptionDao.java
index 0329399..91f3a2e 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/RepairSubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/RepairSubscriptionDao.java
@@ -272,7 +272,7 @@ public class RepairSubscriptionDao extends EntityDaoBase<SubscriptionBundleModel
}
@Override
- public List<SubscriptionBase> getSubscriptions(final UUID bundleId, final InternalTenantContext context) {
+ public List<SubscriptionBase> getSubscriptions(final UUID bundleId, final List<SubscriptionBaseEvent> dryRunEvents, final InternalTenantContext context) {
throw new SubscriptionBaseError(NOT_IMPLEMENTED);
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
index 797c23d..45901e9 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
@@ -61,7 +61,7 @@ public interface SubscriptionDao extends EntityDao<SubscriptionBundleModelDao, S
// SubscriptionBase retrieval
public SubscriptionBase getBaseSubscription(UUID bundleId, InternalTenantContext context);
- public List<SubscriptionBase> getSubscriptions(UUID bundleId, InternalTenantContext context);
+ public List<SubscriptionBase> getSubscriptions(UUID bundleId, List<SubscriptionBaseEvent> dryRunEvents, InternalTenantContext context);
public Map<UUID, List<SubscriptionBase>> getSubscriptionsForAccount(InternalTenantContext context);
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java b/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java
index 6af6942..b3da3d8 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java
@@ -17,6 +17,8 @@
package org.killbill.billing.subscription.events.phase;
+import java.util.UUID;
+
import org.joda.time.DateTime;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
@@ -56,15 +58,15 @@ public class PhaseEventData extends EventBase implements PhaseEvent {
+ ", isActive()=" + isActive() + "]\n";
}
- public static PhaseEvent createNextPhaseEvent(final String phaseName, final DefaultSubscriptionBase subscription, final DateTime now, final DateTime effectiveDate) {
+ public static PhaseEvent createNextPhaseEvent(final UUID subscriptionId, final long activeVersion, final String phaseName, final DateTime now, final DateTime effectiveDate) {
return (phaseName == null) ?
null :
new PhaseEventData(new PhaseEventBuilder()
- .setSubscriptionId(subscription.getId())
+ .setSubscriptionId(subscriptionId)
.setRequestedDate(now)
.setEffectiveDate(effectiveDate)
.setProcessedDate(now)
- .setActiveVersion(subscription.getActiveVersion())
+ .setActiveVersion(activeVersion)
.setPhaseName(phaseName));
}
}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java b/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java
index 8e089ba..e29a18f 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java
@@ -243,7 +243,7 @@ public class TestPlanAligner extends SubscriptionTestSuiteNoDB {
final Plan plan = catalogService.getFullCatalog().findPlan(productName, clock.getUTCNow());
// Same here for the requested date
- final TimedPhase[] phases = planAligner.getCurrentAndNextTimedPhaseOnCreate(defaultSubscriptionBase, plan, initialPhase, priceList, clock.getUTCNow(), effectiveDate);
+ final TimedPhase[] phases = planAligner.getCurrentAndNextTimedPhaseOnCreate(defaultSubscriptionBase.getAlignStartDate(), defaultSubscriptionBase.getBundleStartDate(), plan, initialPhase, priceList, clock.getUTCNow(), effectiveDate);
Assert.assertEquals(phases.length, 2);
return phases;
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/migration/TestMigration.java b/subscription/src/test/java/org/killbill/billing/subscription/api/migration/TestMigration.java
index 22e1e04..727b555 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/migration/TestMigration.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/migration/TestMigration.java
@@ -20,8 +20,6 @@ import java.util.List;
import org.joda.time.DateTime;
import org.joda.time.Interval;
-import org.testng.annotations.Test;
-
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.PhaseType;
@@ -32,10 +30,12 @@ import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi.AccountMigration;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
import org.killbill.billing.subscription.events.user.ApiEventType;
+import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
@@ -45,7 +45,7 @@ import static org.testng.Assert.assertTrue;
public class TestMigration extends SubscriptionTestSuiteWithEmbeddedDB {
@Test(groups = "slow")
- public void testSingleBasePlan() throws SubscriptionBaseMigrationApiException {
+ public void testSingleBasePlan() throws SubscriptionBaseMigrationApiException, SubscriptionBaseApiException {
final DateTime startDate = clock.getUTCNow().minusMonths(2);
final DateTime beforeMigration = clock.getUTCNow();
final AccountMigration toBeMigrated = testUtil.createAccountForMigrationWithRegularBasePlan(startDate);
@@ -59,7 +59,7 @@ public class TestMigration extends SubscriptionTestSuiteWithEmbeddedDB {
assertEquals(bundles.size(), 1);
final SubscriptionBaseBundle bundle = bundles.get(0);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 1);
final SubscriptionBase subscription = subscriptions.get(0);
assertTrue(subscription.getStartDate().compareTo(startDate) == 0);
@@ -74,7 +74,7 @@ public class TestMigration extends SubscriptionTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
- public void testPlanWithAddOn() throws SubscriptionBaseMigrationApiException {
+ public void testPlanWithAddOn() throws SubscriptionBaseMigrationApiException, SubscriptionBaseApiException {
final DateTime beforeMigration = clock.getUTCNow();
final DateTime initalBPStart = clock.getUTCNow().minusMonths(3);
final DateTime initalAddonStart = clock.getUTCNow().minusMonths(1).plusDays(7);
@@ -90,7 +90,7 @@ public class TestMigration extends SubscriptionTestSuiteWithEmbeddedDB {
assertEquals(bundles.size(), 1);
final SubscriptionBaseBundle bundle = bundles.get(0);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 2);
final SubscriptionBase baseSubscription = (subscriptions.get(0).getCurrentPlan().getProduct().getCategory() == ProductCategory.BASE) ?
@@ -119,7 +119,7 @@ public class TestMigration extends SubscriptionTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
- public void testSingleBasePlanFutureCancelled() throws SubscriptionBaseMigrationApiException {
+ public void testSingleBasePlanFutureCancelled() throws SubscriptionBaseMigrationApiException, SubscriptionBaseApiException {
final DateTime startDate = clock.getUTCNow().minusMonths(1);
final DateTime beforeMigration = clock.getUTCNow();
final AccountMigration toBeMigrated = testUtil.createAccountForMigrationWithRegularBasePlanFutreCancelled(startDate);
@@ -134,7 +134,7 @@ public class TestMigration extends SubscriptionTestSuiteWithEmbeddedDB {
final SubscriptionBaseBundle bundle = bundles.get(0);
//assertEquals(bundle.getStartDate(), effectiveDate);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 1);
final SubscriptionBase subscription = subscriptions.get(0);
assertTrue(subscription.getStartDate().compareTo(startDate) == 0);
@@ -163,7 +163,7 @@ public class TestMigration extends SubscriptionTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
- public void testSingleBasePlanWithPendingPhase() throws SubscriptionBaseMigrationApiException {
+ public void testSingleBasePlanWithPendingPhase() throws SubscriptionBaseMigrationApiException, SubscriptionBaseApiException {
final DateTime trialDate = clock.getUTCNow().minusDays(10);
final AccountMigration toBeMigrated = testUtil.createAccountForMigrationFuturePendingPhase(trialDate);
@@ -175,7 +175,7 @@ public class TestMigration extends SubscriptionTestSuiteWithEmbeddedDB {
assertEquals(bundles.size(), 1);
final SubscriptionBaseBundle bundle = bundles.get(0);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 1);
final SubscriptionBase subscription = subscriptions.get(0);
@@ -206,7 +206,7 @@ public class TestMigration extends SubscriptionTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
- public void testSingleBasePlanWithPendingChange() throws SubscriptionBaseMigrationApiException {
+ public void testSingleBasePlanWithPendingChange() throws SubscriptionBaseMigrationApiException, SubscriptionBaseApiException {
final DateTime beforeMigration = clock.getUTCNow();
final AccountMigration toBeMigrated = testUtil.createAccountForMigrationFuturePendingChange();
final DateTime afterMigration = clock.getUTCNow();
@@ -219,7 +219,7 @@ public class TestMigration extends SubscriptionTestSuiteWithEmbeddedDB {
assertEquals(bundles.size(), 1);
final SubscriptionBaseBundle bundle = bundles.get(0);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 1);
final SubscriptionBase subscription = subscriptions.get(0);
//assertDateWithin(subscription.getStartDate(), beforeMigration, afterMigration);
@@ -261,7 +261,7 @@ public class TestMigration extends SubscriptionTestSuiteWithEmbeddedDB {
final List<SubscriptionBaseBundle> bundles = subscriptionInternalApi.getBundlesForAccount(toBeMigrated.getAccountKey(), internalCallContext);
assertEquals(bundles.size(), 1);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundles.get(0).getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundles.get(0).getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 1);
final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptions.get(0);
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java
index af2277c..4cc394d 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java
@@ -20,11 +20,6 @@ import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.Assert;
-import org.testng.annotations.Test;
-
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.PhaseType;
@@ -38,9 +33,12 @@ import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi.AccountMigration;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
@@ -68,7 +66,7 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
final DateTime bundleCreatedDate = bundle.getCreatedDate();
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 1);
final SubscriptionBase subscription = subscriptions.get(0);
testUtil.assertDateWithin(subscription.getStartDate(), beforeMigration.minusMonths(2), afterMigration.minusMonths(2));
@@ -137,7 +135,7 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
assertEquals(bundlesForAccountAndKey.size(), 1);
final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 1);
final SubscriptionBase newBaseSubscription = subscriptions.get(0);
@@ -187,7 +185,7 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 1);
final SubscriptionBase newBaseSubscription = subscriptions.get(0);
@@ -237,7 +235,7 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
assertEquals(bundlesForAccountAndKey.size(), 1);
final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 1);
final SubscriptionBase newBaseSubscription = subscriptions.get(0);
@@ -286,7 +284,7 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
assertEquals(bundlesForAccountAndKey.size(), 1);
final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 1);
final SubscriptionBase newBaseSubscription = subscriptions.get(0);
@@ -379,7 +377,7 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
assertEquals(bundlesForAccountAndKey.size(), 1);
final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 3);
boolean foundBP = false;
boolean foundAO1 = false;
@@ -472,7 +470,7 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
assertEquals(bundlesForAccountAndKey.size(), 1);
final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 1);
}
@@ -516,7 +514,7 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
assertEquals(bundlesForAccountAndKey.size(), 1);
final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
- final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), null, internalCallContext);
assertEquals(subscriptions.size(), 1);
}
}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
index a52cd25..ce52349 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
@@ -216,7 +216,7 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
}
@Override
- public List<SubscriptionBase> getSubscriptions(final UUID bundleId, final InternalTenantContext context) {
+ public List<SubscriptionBase> getSubscriptions(final UUID bundleId, final List<SubscriptionBaseEvent> dryRunEvents, final InternalTenantContext context) {
final List<SubscriptionBase> results = new ArrayList<SubscriptionBase>();
for (final SubscriptionBase cur : subscriptions) {
if (cur.getBundleId().equals(bundleId)) {
diff --git a/util/src/test/java/org/killbill/billing/util/export/dao/TestDatabaseExportDao.java b/util/src/test/java/org/killbill/billing/util/export/dao/TestDatabaseExportDao.java
index 0494ed5..5abfe1a 100644
--- a/util/src/test/java/org/killbill/billing/util/export/dao/TestDatabaseExportDao.java
+++ b/util/src/test/java/org/killbill/billing/util/export/dao/TestDatabaseExportDao.java
@@ -80,7 +80,6 @@ public class TestDatabaseExportDao extends UtilTestSuiteWithEmbeddedDB {
// Verify new dump
final String newDump = getDump();
- // Note: unclear why Jackson would quote the header?
Assert.assertEquals(newDump, "-- accounts record_id,id,external_key,email,name,first_name_length,currency,billing_cycle_day_local,billing_cycle_day_utc,payment_method_id,time_zone,locale,address1,address2,company_name,city,state_or_province,country,postal_code,phone,migrated,is_notified_for_invoices,created_date,created_by,updated_date,updated_by,tenant_record_id\n" +
String.format("%s,\"%s\",,%s,%s,%s,,,,,,,,,,,,,,,false,%s,\"%s\",%s,\"%s\",%s,%s", internalCallContext.getAccountRecordId(), accountId, accountEmail, accountName, firstNameLength,
isNotifiedForInvoices, "1970-05-24T18:33:02.000+0000", createdBy, "1982-02-18T20:03:42.000+0000", updatedBy, internalCallContext.getTenantRecordId()) + "\n" +