killbill-memoizeit
Changes
beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java 438(+438 -0)
invoice/src/test/java/com/ning/billing/invoice/generator/DefaultInvoiceGeneratorWithSwitchRepairLogic.java 72(+72 -0)
Details
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixIntegrationModule.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixIntegrationModule.java
index df8c64a..915cdc2 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixIntegrationModule.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixIntegrationModule.java
@@ -44,6 +44,8 @@ import com.ning.billing.catalog.glue.CatalogModule;
import com.ning.billing.entitlement.api.EntitlementService;
import com.ning.billing.entitlement.glue.DefaultEntitlementModule;
import com.ning.billing.invoice.api.InvoiceService;
+import com.ning.billing.invoice.generator.DefaultInvoiceGeneratorWithSwitchRepairLogic;
+import com.ning.billing.invoice.generator.InvoiceGenerator;
import com.ning.billing.invoice.glue.DefaultInvoiceModule;
import com.ning.billing.junction.glue.DefaultJunctionModule;
import com.ning.billing.lifecycle.KillbillService;
@@ -91,6 +93,7 @@ public class BeatrixIntegrationModule extends AbstractModule {
this.configSource = configSource;
}
+
@Override
protected void configure() {
@@ -112,7 +115,7 @@ public class BeatrixIntegrationModule extends AbstractModule {
install(new AnalyticsModule(configSource));
install(new CatalogModule(configSource));
install(new DefaultEntitlementModule(configSource));
- install(new DefaultInvoiceModule(configSource));
+ install(new DefaultInvoiceModuleWithSwitchRepairLogic(configSource));
install(new TemplateModule());
install(new PaymentPluginMockModule(configSource));
install(new DefaultJunctionModule(configSource));
@@ -141,6 +144,19 @@ public class BeatrixIntegrationModule extends AbstractModule {
bind(DefaultBeatrixService.class).asEagerSingleton();
}
+ private static final class DefaultInvoiceModuleWithSwitchRepairLogic extends DefaultInvoiceModule {
+
+ public DefaultInvoiceModuleWithSwitchRepairLogic(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ protected void installInvoiceGenerator() {
+ bind(InvoiceGenerator.class).to(DefaultInvoiceGeneratorWithSwitchRepairLogic.class).asEagerSingleton();
+ bind(DefaultInvoiceGeneratorWithSwitchRepairLogic.class).asEagerSingleton();
+ }
+ }
+
+
private static final class PaymentPluginMockModule extends PaymentModule {
public PaymentPluginMockModule(final ConfigSource configSource) {
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java
index 1acafee..0296e05 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java
@@ -216,6 +216,7 @@ public class TestBundleTransfer extends TestIntegrationBase {
busHandler.pushExpectedEvent(NextEvent.CANCEL);
busHandler.pushExpectedEvent(NextEvent.TRANSFER);
busHandler.pushExpectedEvent(NextEvent.INVOICE);
+ busHandler.pushExpectedEvent(NextEvent.INVOICE_ADJUSTMENT);
busHandler.pushExpectedEvent(NextEvent.PAYMENT);
transferApi.transferBundle(account.getId(), newAccount.getId(), "mycutebundle", clock.getUTCNow(), false, true, callContext);
assertTrue(busHandler.isCompleted(DELAY));
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestEntitlement.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestEntitlement.java
index 96dc921..9cba8f9 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestEntitlement.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestEntitlement.java
@@ -90,7 +90,7 @@ public class TestEntitlement extends TestIntegrationBase {
//
// FORCE AN IMMEDIATE CHANGE OF THE BILLING PERIOD
//
- busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE);
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT);
assertTrue(bpSubscription.changePlanWithPolicy(productName, BillingPeriod.MONTHLY, planSetName, clock.getUTCNow(), ActionPolicy.IMMEDIATE, callContext));
assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId(), callContext).getCurrentPlan().getBillingPeriod(), BillingPeriod.MONTHLY);
@@ -115,7 +115,7 @@ public class TestEntitlement extends TestIntegrationBase {
//
// FORCE ANOTHER CHANGE
//
- busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE);
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT);
assertTrue(bpSubscription.changePlanWithPolicy(productName, BillingPeriod.ANNUAL, planSetName, clock.getUTCNow(), ActionPolicy.IMMEDIATE, callContext));
assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId(), callContext).getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
assertTrue(busHandler.isCompleted(DELAY));
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
new file mode 100644
index 0000000..6966d9f
--- /dev/null
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning 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 com.ning.billing.beatrix.integration;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.api.TestApiListener.NextEvent;
+import com.ning.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import com.ning.billing.beatrix.util.PaymentChecker.ExpectedPaymentCheck;
+import com.ning.billing.catalog.api.ActionPolicy;
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.catalog.api.PlanPhaseSpecifier;
+import com.ning.billing.catalog.api.PriceListSet;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.entitlement.api.user.SubscriptionData;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceItemType;
+import com.ning.billing.invoice.generator.DefaultInvoiceGeneratorWithSwitchRepairLogic;
+import com.ning.billing.invoice.generator.DefaultInvoiceGeneratorWithSwitchRepairLogic.REPAIR_INVOICE_LOGIC;
+import com.ning.billing.payment.api.Payment;
+import com.ning.billing.payment.api.PaymentStatus;
+
+import com.google.common.collect.ImmutableList;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
+
+ @Inject
+ private DefaultInvoiceGeneratorWithSwitchRepairLogic invoiceGenerator;
+
+
+ @AfterMethod(groups = "slow")
+ public void afterMethod() throws Exception {
+ super.afterMethod();
+ // Make sure to reset to default invoice logic so subsequent test will pass
+ invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
+ }
+
+
+ @Test(groups = "slow")
+ public void testPartialRepairWithCompleteRefund() throws Exception {
+
+ // We take april as it has 30 days (easier to play with BCD)
+ final LocalDate today = new LocalDate(2012, 4, 1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+ clock.setDeltaFromReality(today.toDateTimeAtCurrentTime(DateTimeZone.UTC).getMillis() - clock.getUTCNow().getMillis());
+ final SubscriptionBundle bundle = entitlementUserApi.createBundleForAccount(account.getId(), "whatever", callContext);
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.ANNUAL;
+ final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ //
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+ //
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.INVOICE);
+ final PlanPhaseSpecifier bpPlanPhaseSpecifier = new PlanPhaseSpecifier(productName, ProductCategory.BASE, term, planSetName, null);
+ final SubscriptionData bpSubscription = subscriptionDataFromSubscription(entitlementUserApi.createSubscription(bundle.getId(),
+ bpPlanPhaseSpecifier,
+ null,
+ callContext));
+ assertNotNull(bpSubscription);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+ assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
+
+ assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId(), callContext).getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
+
+ // Move out of trials for interesting invoices adjustments
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+ clock.addDays(30);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 2);
+ ImmutableList<ExpectedInvoiceItemCheck> 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);
+
+ //
+ // FORCE AN IMMEDIATE CHANGE OF THE BILLING PERIOD
+ //
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT);
+ assertTrue(bpSubscription.changePlanWithPolicy(productName, BillingPeriod.MONTHLY, planSetName, clock.getUTCNow(), ActionPolicy.IMMEDIATE, callContext));
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId(), callContext).getCurrentPlan().getBillingPeriod(), BillingPeriod.MONTHLY);
+
+
+ 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")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2399.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("2399.95")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ clock.addMonths(1);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 4);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2399.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("2399.95")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 6, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ clock.addMonths(1);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 5);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2399.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("2399.95")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 6, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 1), new LocalDate(2012, 8, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 1), new LocalDate(2012, 7, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
+ invoiceChecker.checkInvoice(invoices.get(4).getId(), callContext, toBeChecked);
+ }
+
+ @Test(groups = "slow")
+ public void testInvoiceLogicWithFullRepairFollowedByPartialRepair() throws Exception {
+
+ // START TEST WITH OLD FULL_REPAIR LOGIC
+ invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.FULL_REPAIR);
+
+ final LocalDate today = new LocalDate(2012, 4, 1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+ clock.setDeltaFromReality(today.toDateTimeAtCurrentTime(DateTimeZone.UTC).getMillis() - clock.getUTCNow().getMillis());
+ final SubscriptionBundle bundle = entitlementUserApi.createBundleForAccount(account.getId(), "whatever", callContext);
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.ANNUAL;
+ final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ //
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+ //
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.INVOICE);
+ final PlanPhaseSpecifier bpPlanPhaseSpecifier = new PlanPhaseSpecifier(productName, ProductCategory.BASE, term, planSetName, null);
+ final SubscriptionData bpSubscription = subscriptionDataFromSubscription(entitlementUserApi.createSubscription(bundle.getId(),
+ bpPlanPhaseSpecifier,
+ null,
+ callContext));
+ assertNotNull(bpSubscription);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+ assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
+
+ assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId(), callContext).getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
+
+ // Move out of trials for interesting invoices adjustments
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+ clock.addDays(40);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 2);
+ ImmutableList<ExpectedInvoiceItemCheck> 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);
+
+ //
+ // FORCE AN IMMEDIATE CHANGE OF THE BILLING PERIOD
+ //
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT);
+ assertTrue(bpSubscription.changePlanWithPolicy(productName, BillingPeriod.MONTHLY, planSetName, clock.getUTCNow(), ActionPolicy.IMMEDIATE, callContext));
+ assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId(), callContext).getCurrentPlan().getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ 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")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2399.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("2399.95")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 11), InvoiceItemType.RECURRING, new BigDecimal("65.76")),
+ 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(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-235.08")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+ // NOW SWITCH BACK TO PARTIAL REPAIR LOGIC AND GENERATE NEXT 2 INVOICES
+ invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ clock.addMonths(1);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 4);
+
+ // RECHECK PREVIOUS INVOICE DID NOT CHANGE
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2399.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("2399.95")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 11), InvoiceItemType.RECURRING, new BigDecimal("65.76")),
+ 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(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-235.08")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+ // AND THEN CHECK NEW INVOICE
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 11), new LocalDate(2012, 6, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ clock.addMonths(1);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 5);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 1), new LocalDate(2012, 8, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 11), new LocalDate(2012, 7, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
+ invoiceChecker.checkInvoice(invoices.get(4).getId(), callContext, toBeChecked);
+ }
+
+
+ @Test(groups = "slow")
+ public void testInvoiceLogicWithFullRepairFollowedByPartialRepairWithItemAdjustment() throws Exception {
+
+ // START TEST WITH OLD FULL_REPAIR LOGIC
+ invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.FULL_REPAIR);
+
+ final LocalDate today = new LocalDate(2012, 4, 1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+ clock.setDeltaFromReality(today.toDateTimeAtCurrentTime(DateTimeZone.UTC).getMillis() - clock.getUTCNow().getMillis());
+ final SubscriptionBundle bundle = entitlementUserApi.createBundleForAccount(account.getId(), "whatever", callContext);
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.ANNUAL;
+ final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ //
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+ //
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.INVOICE);
+ final PlanPhaseSpecifier bpPlanPhaseSpecifier = new PlanPhaseSpecifier(productName, ProductCategory.BASE, term, planSetName, null);
+ final SubscriptionData bpSubscription = subscriptionDataFromSubscription(entitlementUserApi.createSubscription(bundle.getId(),
+ bpPlanPhaseSpecifier,
+ null,
+ callContext));
+ assertNotNull(bpSubscription);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+ assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
+
+ assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId(), callContext).getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
+
+ // Move out of trials for interesting invoices adjustments
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+ clock.addDays(40);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 2);
+ ImmutableList<ExpectedInvoiceItemCheck> 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);
+
+ //
+ // ITEM ADJUSTMENT PRIOR TO DOING THE REPAIR
+ //
+ final Invoice invoice1 = invoices.get(1);
+ final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), callContext);
+ final ExpectedPaymentCheck expectedPaymentCheck = new ExpectedPaymentCheck(clock.getUTCNow().toLocalDate(), new BigDecimal("2399.95"), PaymentStatus.SUCCESS, invoice1.getId(), Currency.USD);
+ final Payment payment1 = payments.get(0);
+
+
+ final Map<UUID, BigDecimal> iias = new HashMap<UUID, BigDecimal>();
+ iias.put(invoice1.getInvoiceItems().get(0).getId(), new BigDecimal("10.00"));
+ busHandler.pushExpectedEvents(NextEvent.INVOICE_ADJUSTMENT);
+ paymentApi.createRefundWithItemsAdjustments(account, payment1.getId(), iias, callContext);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 2);
+
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+ // TODO SETPH the ITEM_ADJ seems to be created with the context getCreatedDate()
+ new ExpectedInvoiceItemCheck(callContext.getCreatedDate().toLocalDate(), callContext.getCreatedDate().toLocalDate(), InvoiceItemType.ITEM_ADJ, new BigDecimal("-10.00")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+
+ //
+ // FORCE AN IMMEDIATE CHANGE OF THE BILLING PERIOD
+ //
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT);
+ assertTrue(bpSubscription.changePlanWithPolicy(productName, BillingPeriod.MONTHLY, planSetName, clock.getUTCNow(), ActionPolicy.IMMEDIATE, callContext));
+ assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId(), callContext).getCurrentPlan().getBillingPeriod(), BillingPeriod.MONTHLY);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ 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")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2389.95")),
+ new ExpectedInvoiceItemCheck(callContext.getCreatedDate().toLocalDate(), callContext.getCreatedDate().toLocalDate(), InvoiceItemType.ITEM_ADJ, new BigDecimal("-10.00")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("2389.95")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 11), InvoiceItemType.RECURRING, new BigDecimal("65.76")),
+ 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(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-235.08")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+ // NOW SWITCH BACK TO PARTIAL REPAIR LOGIC AND GENERATE NEXT 2 INVOICES
+ invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ clock.addMonths(1);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 4);
+
+ // RECHECK PREVIOUS INVOICE DID NOT CHANGE
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2389.95")),
+ new ExpectedInvoiceItemCheck(callContext.getCreatedDate().toLocalDate(), callContext.getCreatedDate().toLocalDate(), InvoiceItemType.ITEM_ADJ, new BigDecimal("-10.00")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("2389.95")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 11), InvoiceItemType.RECURRING, new BigDecimal("65.76")),
+ 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(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-235.08")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+ // AND THEN CHECK NEW INVOICE
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 11), new LocalDate(2012, 6, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
+ }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java b/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java
index ac10e99..84dedc8 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java
@@ -52,6 +52,8 @@ import com.ning.billing.util.svcapi.junction.BillingEvent;
import com.ning.billing.util.svcapi.junction.BillingEventSet;
import com.ning.billing.util.svcapi.junction.BillingModeType;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.inject.Inject;
@@ -220,16 +222,27 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
* @param invoiceItem any invoice item to compare to
* @return true if invoiceItem is the reparee for that repaired invoice item
*/
- private boolean isRepareeItemForRepairedItem(final InvoiceItem repairedInvoiceItem, final InvoiceItem invoiceItem) {
- return repairedInvoiceItem.getInvoiceItemType().equals(invoiceItem.getInvoiceItemType()) &&
+ @VisibleForTesting
+ boolean isRepareeItemForRepairedItem(final InvoiceItem repairedInvoiceItem, final InvoiceItem invoiceItem) {
+ return !repairedInvoiceItem.getId().equals(invoiceItem.getId()) &&
+ repairedInvoiceItem.getInvoiceItemType().equals(invoiceItem.getInvoiceItemType()) &&
+ // We assume the items are correctly created, so that the subscription id check implicitly
+ // verifies that account id and bundle id matches
repairedInvoiceItem.getSubscriptionId().equals(invoiceItem.getSubscriptionId()) &&
+ // The reparee item is the "portion used" of the repaired item, hence it will have the same start date
repairedInvoiceItem.getStartDate().compareTo(invoiceItem.getStartDate()) == 0 &&
- // FIXED items have a null end date
+ // Similarly, check the "portion used" is less than the original service end date. The check
+ // is strict, otherwise there wouldn't be anything to repair
((repairedInvoiceItem.getEndDate() == null && invoiceItem.getEndDate() == null) ||
(repairedInvoiceItem.getEndDate() != null && invoiceItem.getEndDate() != null &&
- // We need to look for stricly after otherwsie we could return thew new item for that period in case of a complete repair
repairedInvoiceItem.getEndDate().isAfter(invoiceItem.getEndDate()))) &&
- !repairedInvoiceItem.getId().equals(invoiceItem.getId());
+ // Finally, for the tricky part... In case of complete repairs, the new item will always meet all of the
+ // following conditions: same type, subscription, start date. Depending on the catalog configuration, the end
+ // date check could also match (e.g. repair from annual to monthly). For that scenario, we need to default
+ // to catalog checks (the rate check is a lame check for versioned catalogs).
+ Objects.firstNonNull(repairedInvoiceItem.getPlanName(), "").equals(Objects.firstNonNull(invoiceItem.getPlanName(), "")) &&
+ Objects.firstNonNull(repairedInvoiceItem.getPhaseName(), "").equals(Objects.firstNonNull(invoiceItem.getPhaseName(), "")) &&
+ Objects.firstNonNull(repairedInvoiceItem.getRate(), BigDecimal.ZERO).compareTo(Objects.firstNonNull(invoiceItem.getRate(), BigDecimal.ZERO)) == 0;
}
@@ -355,7 +368,7 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
* @param repairedItem the repaired item
* @param proposedItems the list of existing items
*/
- private void removeProposedRepareeForPartialrepair(final InvoiceItem repairedItem, final List<InvoiceItem> proposedItems) {
+ protected void removeProposedRepareeForPartialrepair(final InvoiceItem repairedItem, final List<InvoiceItem> proposedItems) {
final Iterator<InvoiceItem> it = proposedItems.iterator();
while (it.hasNext()) {
final InvoiceItem cur = it.next();
diff --git a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
index 78dee0c..4d6b261 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
@@ -22,8 +22,12 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
import java.util.UUID;
+import javax.annotation.Nullable;
+
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
@@ -42,6 +46,7 @@ import com.ning.billing.invoice.api.InvoiceItem;
import com.ning.billing.invoice.api.InvoiceItemType;
import com.ning.billing.invoice.api.InvoiceNotifier;
import com.ning.billing.invoice.api.InvoicePayment;
+import com.ning.billing.invoice.api.user.DefaultInvoiceAdjustmentEvent;
import com.ning.billing.invoice.api.user.DefaultInvoiceCreationEvent;
import com.ning.billing.invoice.api.user.DefaultNullInvoiceEvent;
import com.ning.billing.invoice.dao.InvoiceDao;
@@ -56,7 +61,8 @@ import com.ning.billing.util.callcontext.InternalCallContext;
import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.events.BusInternalEvent;
import com.ning.billing.util.events.EffectiveSubscriptionInternalEvent;
-import com.ning.billing.util.events.InvoiceCreationInternalEvent;
+import com.ning.billing.util.events.InvoiceAdjustmentInternalEvent;
+import com.ning.billing.util.events.InvoiceInternalEvent;
import com.ning.billing.util.globallocker.GlobalLock;
import com.ning.billing.util.globallocker.GlobalLocker;
import com.ning.billing.util.globallocker.GlobalLocker.LockerType;
@@ -70,7 +76,6 @@ import com.ning.billing.util.svcsapi.bus.InternalBus.EventBusException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
-import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
@@ -188,15 +193,20 @@ public class InvoiceDispatcher {
}
} else {
log.info("Generated invoice {} with {} items for accountId {} and targetDate {} (targetDateTime {})", new Object[]{invoice.getId(), invoice.getNumberOfItems(),
- accountId, targetDate, targetDateTime});
+ accountId, targetDate, targetDateTime});
if (!dryRun) {
- // We need to check whether this is just a 'shell' invoice or a real invoice with items on it
- final boolean isRealInvoiceWithItems = Collections2.filter(invoice.getInvoiceItems(), new Predicate<InvoiceItem>() {
+
+ // 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>();
+ adjustedUniqueOtherInvoiceId.addAll(Collections2.transform(invoice.getInvoiceItems(), new Function<InvoiceItem, UUID>() {
+ @Nullable
@Override
- public boolean apply(final InvoiceItem input) {
- return input.getInvoiceId().equals(invoice.getId());
+ public UUID apply(@Nullable final InvoiceItem input) {
+ return input.getInvoiceId();
}
- }).size() > 0;
+ }));
+ final boolean isRealInvoiceWithItems = adjustedUniqueOtherInvoiceId.remove(invoice.getId());
+
final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoice);
final List<InvoiceItemModelDao> invoiceItemModelDaos = ImmutableList.<InvoiceItemModelDao>copyOf(Collections2.transform(invoice.getInvoiceItems(),
@@ -222,13 +232,22 @@ public class InvoiceDispatcher {
final List<InvoiceItem> recurringInvoiceItems = invoice.getInvoiceItems(RecurringInvoiceItem.class);
setChargedThroughDates(dateAndTimeZoneContext, fixedPriceInvoiceItems, recurringInvoiceItems, context);
- final InvoiceCreationInternalEvent event = new DefaultInvoiceCreationEvent(invoice.getId(), invoice.getAccountId(),
- invoice.getBalance(), invoice.getCurrency(),
- context.getUserToken(),
- context.getAccountRecordId(),
- context.getTenantRecordId());
+ final List<InvoiceInternalEvent> events = new ArrayList<InvoiceInternalEvent>();
if (isRealInvoiceWithItems) {
+ events.add(new DefaultInvoiceCreationEvent(invoice.getId(), invoice.getAccountId(),
+ invoice.getBalance(), invoice.getCurrency(),
+ context.getUserToken(),
+ context.getAccountRecordId(),
+ context.getTenantRecordId()));
+ }
+ for (UUID cur : adjustedUniqueOtherInvoiceId) {
+ final InvoiceAdjustmentInternalEvent event = new DefaultInvoiceAdjustmentEvent(cur, invoice.getAccountId(), context.getUserToken(), context.getAccountRecordId(), context.getTenantRecordId());
+ events.add(event);
+ }
+
+
+ for (InvoiceInternalEvent event : events) {
postEvent(event, accountId, context);
}
}
@@ -321,6 +340,7 @@ public class InvoiceDispatcher {
this.referenceTime = effectiveDateTime != null ? effectiveDateTime.toLocalTime() : null;
this.accountTimeZone = accountTimeZone;
}
+
public LocalDate computeTargetDate(final DateTime targetDateTime) {
return new LocalDate(targetDateTime, accountTimeZone);
}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/generator/DefaultInvoiceGeneratorWithSwitchRepairLogic.java b/invoice/src/test/java/com/ning/billing/invoice/generator/DefaultInvoiceGeneratorWithSwitchRepairLogic.java
new file mode 100644
index 0000000..f6f95ec
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/generator/DefaultInvoiceGeneratorWithSwitchRepairLogic.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning 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 com.ning.billing.invoice.generator;
+
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.config.InvoiceConfig;
+
+import com.google.inject.Inject;
+
+public class DefaultInvoiceGeneratorWithSwitchRepairLogic extends DefaultInvoiceGenerator {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultInvoiceGeneratorWithSwitchRepairLogic.class);
+
+ private REPAIR_INVOICE_LOGIC repairtLogic;
+
+ public enum REPAIR_INVOICE_LOGIC {
+ FULL_REPAIR,
+ PARTIAL_REPAIR
+ }
+
+
+ @Inject
+ public DefaultInvoiceGeneratorWithSwitchRepairLogic(final Clock clock, final InvoiceConfig config) {
+ super(clock, config);
+ // Default DefaultInvoicegenerator repair logic
+ setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
+ }
+
+ @Override
+ protected void removeProposedRepareeForPartialrepair(final InvoiceItem repairedItem, final List<InvoiceItem> proposedItems) {
+ if (repairtLogic == REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR) {
+ super.removeProposedRepareeForPartialrepair(repairedItem, proposedItems);
+ }
+ }
+
+ @Override
+ void addRepairItem(final InvoiceItem repairedItem, final RepairAdjInvoiceItem candidateRepairItem, final List<InvoiceItem> proposedItems) {
+ if (repairtLogic == REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR) {
+ super.addRepairItem(repairedItem, candidateRepairItem, proposedItems);
+ } else {
+ proposedItems.add(candidateRepairItem);
+ return;
+ }
+ }
+
+
+ public void setDefaultRepairLogic(final REPAIR_INVOICE_LOGIC repairLogic) {
+ log.info("Switching DefaultInvoiceGenerator repair logic to : " + repairLogic);
+ this.repairtLogic = repairLogic;
+ }
+}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorUnit.java b/invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorUnit.java
index 909ed5a..8d12312 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorUnit.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorUnit.java
@@ -26,16 +26,15 @@ import org.testng.annotations.Test;
import com.ning.billing.catalog.api.Currency;
import com.ning.billing.invoice.InvoiceTestSuiteNoDB;
-import com.ning.billing.invoice.api.Invoice;
import com.ning.billing.invoice.api.InvoiceItem;
import com.ning.billing.invoice.api.InvoiceItemType;
import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
import com.ning.billing.invoice.model.RecurringInvoiceItem;
import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
public class TestDefaultInvoiceGeneratorUnit extends InvoiceTestSuiteNoDB {
@@ -256,7 +255,41 @@ public class TestDefaultInvoiceGeneratorUnit extends InvoiceTestSuiteNoDB {
assertEquals(newItem2.getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
assertEquals(newItem2.getAmount(), item1.getAmount().negate());
assertEquals(newItem2.getLinkedItemId(), item1.getId());
+ }
+ @Test(groups = "fast")
+ public void testShouldFindRepareeForPartialRepairs() throws Exception {
+ final LocalDate startDate = new LocalDate(2012, 5, 1);
+ final LocalDate endDate = new LocalDate(2012, 6, 1);
+ // Repaired item
+ final InvoiceItem silver = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, BigDecimal.TEN, BigDecimal.TEN, currency);
+
+ // Reparee item
+ final LocalDate actualEndDateSilver = new LocalDate(2012, 5, 10);
+ final InvoiceItem actualSilver = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, actualEndDateSilver, new BigDecimal("3"), BigDecimal.TEN, currency);
+
+ // New item
+ final InvoiceItem gold = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "new-" + planName, phaseName, actualEndDateSilver, endDate, BigDecimal.TEN, new BigDecimal("15"), currency);
+
+ assertFalse(((DefaultInvoiceGenerator) generator).isRepareeItemForRepairedItem(silver, silver));
+ assertFalse(((DefaultInvoiceGenerator) generator).isRepareeItemForRepairedItem(silver, gold));
+ assertTrue(((DefaultInvoiceGenerator) generator).isRepareeItemForRepairedItem(silver, actualSilver));
}
+ @Test(groups = "fast")
+ public void testShouldntFindRepareeForFullRepairs() throws Exception {
+ final LocalDate startDate = new LocalDate(2012, 5, 1);
+ final LocalDate endDate = new LocalDate(2013, 5, 1);
+ // Repaired item
+ final InvoiceItem annual = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, BigDecimal.TEN, BigDecimal.TEN, currency);
+
+ // There is no reparee - full repair
+
+ // New item
+ final LocalDate endDate2 = new LocalDate(2012, 6, 1);
+ final InvoiceItem monthly = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "new-" + planName, phaseName, startDate, endDate2, BigDecimal.TEN, BigDecimal.TEN, currency);
+
+ assertFalse(((DefaultInvoiceGenerator) generator).isRepareeItemForRepairedItem(annual, annual));
+ assertFalse(((DefaultInvoiceGenerator) generator).isRepareeItemForRepairedItem(annual, monthly));
+ }
}
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
index e132e4e..62e7f91 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
@@ -220,7 +220,7 @@ public class TestJaxrsBase extends KillbillClient {
public void beforeMethod() throws Exception {
super.beforeMethod();
busHandler.reset();
- clock.reset();
+ clock.resetDeltaFromReality();
clock.setDay(new LocalDate(2012, 8, 25));
}
diff --git a/util/src/test/java/com/ning/billing/util/clock/ClockMock.java b/util/src/test/java/com/ning/billing/util/clock/ClockMock.java
index 4552087..1661769 100644
--- a/util/src/test/java/com/ning/billing/util/clock/ClockMock.java
+++ b/util/src/test/java/com/ning/billing/util/clock/ClockMock.java
@@ -21,7 +21,6 @@ import org.joda.time.DateTimeZone;
import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.joda.time.Months;
-import org.joda.time.MutablePeriod;
import org.joda.time.Period;
import org.joda.time.ReadablePeriod;
import org.joda.time.Weeks;
@@ -29,13 +28,17 @@ import org.joda.time.Years;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.ning.billing.catalog.api.Duration;
-import com.ning.billing.catalog.api.TimeUnit;
-
public class ClockMock implements Clock {
- private MutablePeriod delta = new MutablePeriod();
+
private static final Logger log = LoggerFactory.getLogger(ClockMock.class);
+ private DateTime mockDateTime;
+ private long initialDeltaMillis;
+
+ public ClockMock() {
+ reset();
+ }
+
@Override
public synchronized DateTime getNow(final DateTimeZone tz) {
return getUTCNow().toDateTime(tz);
@@ -43,7 +46,7 @@ public class ClockMock implements Clock {
@Override
public synchronized DateTime getUTCNow() {
- return truncate(adjust(now()));
+ return truncate(mockDateTime.plus(System.currentTimeMillis() - initialDeltaMillis));
}
@Override
@@ -56,18 +59,9 @@ public class ClockMock implements Clock {
return new LocalDate(getUTCNow(), timeZone);
}
- private DateTime adjust(final DateTime now) {
- return now.plus(delta);
- }
-
- public synchronized void setTime(final DateTime time) {
- final DateTime prev = getUTCNow();
- delta = new MutablePeriod(now(), time);
- logChange(prev);
- }
-
- public synchronized void setDay(final LocalDate date) {
- setTime(date.toDateTimeAtStartOfDay(DateTimeZone.UTC));
+ @Override
+ public String toString() {
+ return getUTCNow().toString();
}
public synchronized void addDays(final int days) {
@@ -86,77 +80,54 @@ public class ClockMock implements Clock {
adjustTo(Years.years(years));
}
- public synchronized void reset() {
- delta = new MutablePeriod();
+ public synchronized void setDeltaFromReality(final long delta) {
+ resetDeltaFromReality();
+ addDeltaFromReality(delta);
}
- @Override
- public String toString() {
- return getUTCNow().toString();
+ public synchronized void addDeltaFromReality(final long delta) {
+ adjustTo(new Period(delta));
}
- private void adjustTo(final ReadablePeriod period) {
+ public synchronized void setDay(final LocalDate date) {
+ setTime(date.toDateTimeAtStartOfDay(DateTimeZone.UTC));
+ }
+
+ public synchronized void setTime(final DateTime time) {
final DateTime prev = getUTCNow();
- delta.add(period);
+ reset(time);
logChange(prev);
}
- private void logChange(final DateTime prev) {
- final DateTime now = getUTCNow();
- log.info(String.format(" ************ ADJUSTING CLOCK FROM %s to %s ********************", prev, now));
+ public synchronized void resetDeltaFromReality() {
+ reset();
}
- private DateTime now() {
- return new DateTime(DateTimeZone.UTC);
+ private synchronized void reset() {
+ reset(realNow());
}
- private DateTime truncate(final DateTime time) {
- return time.minus(time.getMillisOfSecond());
+ private void reset(final DateTime time) {
+ mockDateTime = time;
+ initialDeltaMillis = System.currentTimeMillis();
}
- //
- //Backward compatibility stuff
- //
- public synchronized void setDeltaFromReality(final Duration duration, final long epsilon) {
+ private void adjustTo(final ReadablePeriod period) {
final DateTime prev = getUTCNow();
- delta.addMillis((int) epsilon);
- addDeltaFromReality(duration);
+ mockDateTime = mockDateTime.plus(period);
logChange(prev);
-
- }
-
- public synchronized void addDeltaFromReality(final Duration delta) {
- adjustTo(periodFromDuration(delta));
}
- public synchronized void setDeltaFromReality(final long delta) {
- adjustTo(new Period(delta));
- }
-
- public synchronized void addDeltaFromReality(final long delta) {
- adjustTo(new Period(delta));
+ private void logChange(final DateTime prev) {
+ final DateTime now = getUTCNow();
+ log.info(String.format(" ************ ADJUSTING CLOCK FROM %s to %s ********************", prev, now));
}
- public synchronized void resetDeltaFromReality() {
- reset();
+ private DateTime truncate(final DateTime time) {
+ return time.minus(time.getMillisOfSecond());
}
- public ReadablePeriod periodFromDuration(final Duration duration) {
- if (duration.getUnit() != TimeUnit.UNLIMITED) {
- return new Period();
- }
-
- switch (duration.getUnit()) {
- case DAYS:
- return Days.days(duration.getNumber());
- case MONTHS:
- return Months.months(duration.getNumber());
- case YEARS:
- return Years.years(duration.getNumber());
- case UNLIMITED:
- return Years.years(100);
- default:
- return new Period();
- }
+ private DateTime realNow() {
+ return new DateTime(DateTimeZone.UTC);
}
}
diff --git a/util/src/test/java/com/ning/billing/util/clock/TestClockMock.java b/util/src/test/java/com/ning/billing/util/clock/TestClockMock.java
new file mode 100644
index 0000000..ad5e9db
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/clock/TestClockMock.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning 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 com.ning.billing.util.clock;
+
+import java.util.concurrent.Callable;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.util.UtilTestSuiteNoDB;
+
+import com.jayway.awaitility.Awaitility;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+public class TestClockMock extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testBasicClockOperations() throws Exception {
+ final ClockMock clock = new ClockMock();
+
+ final DateTime startingTime = new DateTime(DateTimeZone.UTC);
+ // Lame, but required due to the truncation magic
+ Awaitility.await().atMost(999, MILLISECONDS).until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return clock.getUTCNow().isAfter(startingTime);
+ }
+ });
+
+ clock.setTime(new DateTime(2012, 5, 1, 1, 2, 3, DateTimeZone.UTC));
+ Assert.assertEquals(clock.getUTCToday(), new LocalDate(2012, 5, 1));
+ final DateTime utcNowAfterSetTime = clock.getUTCNow();
+ Assert.assertEquals(utcNowAfterSetTime.getHourOfDay(), 1);
+ Assert.assertEquals(utcNowAfterSetTime.getMinuteOfHour(), 2);
+ Assert.assertEquals(utcNowAfterSetTime.getSecondOfMinute(), 3);
+
+ clock.addDays(1);
+ Assert.assertEquals(clock.getUTCToday(), new LocalDate(2012, 5, 2));
+
+ clock.addMonths(1);
+ Assert.assertEquals(clock.getUTCToday(), new LocalDate(2012, 6, 2));
+
+ clock.addYears(1);
+ Assert.assertEquals(clock.getUTCToday(), new LocalDate(2013, 6, 2));
+
+ clock.setDay(new LocalDate(2045, 12, 12));
+ Assert.assertEquals(clock.getUTCToday(), new LocalDate(2045, 12, 12));
+
+ clock.resetDeltaFromReality();
+ Assert.assertTrue(clock.getUTCNow().isAfter(startingTime));
+ Assert.assertTrue(clock.getUTCNow().isBefore(startingTime.plusMinutes(1)));
+ }
+}