killbill-aplcache
Changes
beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java 19(+10 -9)
beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java 251(+10 -241)
invoice/src/test/java/com/ning/billing/invoice/generator/DefaultInvoiceGeneratorWithSwitchRepairLogic.java 65(+0 -65)
invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorRepairUnit.java 474(+0 -474)
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 c52cdbd..3dca515 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
@@ -42,7 +42,7 @@ import com.ning.billing.currency.glue.CurrencyModule;
import com.ning.billing.entitlement.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.DefaultInvoiceGenerator;
import com.ning.billing.invoice.generator.InvoiceGenerator;
import com.ning.billing.invoice.glue.DefaultInvoiceModule;
import com.ning.billing.junction.glue.DefaultJunctionModule;
@@ -144,8 +144,7 @@ public class BeatrixIntegrationModule extends AbstractModule {
}
protected void installInvoiceGenerator() {
- bind(InvoiceGenerator.class).to(DefaultInvoiceGeneratorWithSwitchRepairLogic.class).asEagerSingleton();
- bind(DefaultInvoiceGeneratorWithSwitchRepairLogic.class).asEagerSingleton();
+ bind(InvoiceGenerator.class).to(DefaultInvoiceGenerator.class).asEagerSingleton();
}
}
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index 4d94e20..cc2a894 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -181,8 +181,8 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkInvoice(account.getId(), 3, callContext,
new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 23), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-102.13")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("102.13")));
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 23), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-104.83")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("104.83")));
// Add 10 days to generate next invoice. We verify that we indeed have a notification for nextBillingDate
addDaysAndCheckForCompletion(10, NextEvent.INVOICE, NextEvent.PAYMENT);
@@ -190,7 +190,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkInvoice(account.getId(), 4, callContext,
// Item for the upgraded recurring plan
new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 31), new LocalDate(2012, 8, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 8, 2), new LocalDate(2012, 8, 2), InvoiceItemType.CBA_ADJ, new BigDecimal("-102.13")));
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 8, 2), new LocalDate(2012, 8, 2), InvoiceItemType.CBA_ADJ, new BigDecimal("-104.83")));
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 8, 31), callContext);
@@ -465,16 +465,16 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkInvoice(account.getId(), 3, callContext,
new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 23), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-102.13")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("102.13")));
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 23), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-104.83")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("104.83")));
// Do an upgrade now
checkChangePlanWithOverdueState(baseEntitlement, false, false);
invoiceChecker.checkRepairedInvoice(account.getId(), 3, callContext,
new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 23), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-102.13")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("102.13")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 23), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-104.83")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("104.83")),
new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 31), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-64.51")),
new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("64.51")));
@@ -487,7 +487,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 7, 31), callContext);
// Verify the account balance:
- assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(new BigDecimal("-11.79")), 0);
+ assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(new BigDecimal("-14.49")), 0);
}
@Test(groups = "slow", description = "Test overdue stages and follow with an immediate change of plan and use of credit", enabled=false)
@@ -539,6 +539,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
allowPaymentsAndResetOverdueToClearByPayingAllUnpaidInvoices(false);
+
invoiceChecker.checkInvoice(account.getId(), 2, callContext,
// New invoice for the part that was unblocked up to the BCD
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2013, 5, 31), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
@@ -664,7 +665,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 7, 31), callContext);
- assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(new BigDecimal("-8.88")), 0);
+ assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(new BigDecimal("-12.91")), 0);
}
@Test(groups = "slow", description = "Test overdue from non paid external charge")
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
index c6d30c9..77104d0 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
@@ -111,7 +111,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
protected static final Logger log = LoggerFactory.getLogger(TestIntegrationBase.class);
protected static long AT_LEAST_ONE_MONTH_MS = 32L * 24L * 3600L * 1000L;
- protected static final long DELAY = 10000;
+ protected static final long DELAY = 10000; // * 100000;
@Inject
protected Lifecycle lifecycle;
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
index d060568..ab23915 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
@@ -22,8 +22,6 @@ 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.Assert;
@@ -45,8 +43,6 @@ import com.ning.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
import com.ning.billing.invoice.api.Invoice;
import com.ning.billing.invoice.api.InvoiceApiException;
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;
@@ -57,14 +53,9 @@ import static org.testng.Assert.assertNotNull;
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")
@@ -138,7 +129,6 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
// Force a plan change
//
changeEntitlementAndCheckForCompletion(bpEntitlement, "Blowdart", term, BillingActionPolicy.IMMEDIATE, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT);
-
invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
assertEquals(invoices.size(), 3);
@@ -151,9 +141,10 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 2), new LocalDate(2012, 5, 2), InvoiceItemType.ITEM_ADJ, new BigDecimal("-10")),
// TODO PIERRE The cba start_date/end_date are created using the callcontext
new ExpectedInvoiceItemCheck(callContext.getCreatedDate().toLocalDate(), callContext.getCreatedDate().toLocalDate(), InvoiceItemType.CBA_ADJ, new BigDecimal("10")),
- // You can check here that 239.95 - 249.95/31 = 231.88
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 2), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-231.89")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 2), new LocalDate(2012, 5, 2), InvoiceItemType.CBA_ADJ, new BigDecimal("231.89")));
+ // The pro-rated piece is ~ 249.95 - (249.95 / 31) ~ 241.88. However we adjusted the item so max available amount is 249.95 - 10 = 239.95.
+ // So we consume all of it since max amount is less than 241.88.
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 2), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-239.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 2), new LocalDate(2012, 5, 2), InvoiceItemType.CBA_ADJ, new BigDecimal("239.95")));
invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
@@ -286,8 +277,8 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 7), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("8.02")),
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 7), new LocalDate(2012, 5, 7), InvoiceItemType.CBA_ADJ, new BigDecimal("-8.02")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-7.69")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 5, 8), InvoiceItemType.CBA_ADJ, new BigDecimal("7.69")));
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-7.70")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 5, 8), InvoiceItemType.CBA_ADJ, new BigDecimal("7.70")));
invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
@@ -323,8 +314,8 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 7), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("8.02")),
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 7), new LocalDate(2012, 5, 7), InvoiceItemType.CBA_ADJ, new BigDecimal("-8.02")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-7.69")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 5, 8), InvoiceItemType.CBA_ADJ, new BigDecimal("7.69")));
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-7.70")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 5, 8), InvoiceItemType.CBA_ADJ, new BigDecimal("7.70")));
invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
@@ -365,8 +356,8 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 7), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("8.02")),
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 7), new LocalDate(2012, 5, 7), InvoiceItemType.CBA_ADJ, new BigDecimal("-8.02")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-7.69")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 5, 8), InvoiceItemType.CBA_ADJ, new BigDecimal("7.69")));
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-7.70")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 5, 8), InvoiceItemType.CBA_ADJ, new BigDecimal("7.70")));
invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
@@ -494,222 +485,8 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
}
@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 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
- //
- DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
- assertNotNull(bpEntitlement);
-
- assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
-
- assertEquals(bpEntitlement.getSubscriptionBase().getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
-
- // Move out of trials for interesting invoices adjustments
- busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
- clock.addDays(40);
- 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
- //
- changeEntitlementAndCheckForCompletion(bpEntitlement, productName, BillingPeriod.MONTHLY, BillingActionPolicy.IMMEDIATE, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT);
-
- 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.75")),
- 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.07")));
- 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);
- 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.75")),
- 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.07")));
- 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);
- 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 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
- //
- DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
- assertNotNull(bpEntitlement);
- assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
-
- assertEquals(bpEntitlement.getSubscriptionBase().getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
-
- // Move out of trials for interesting invoices adjustments
- busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
- clock.addDays(40);
- 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);
- 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 callcontext 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
- //
- changeEntitlementAndCheckForCompletion(bpEntitlement, productName, BillingPeriod.MONTHLY, BillingActionPolicy.IMMEDIATE, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT);
-
- 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.75")),
- 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.07")));
- 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);
- 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.75")),
- 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.07")));
- 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);
- }
-
- @Test(groups = "slow")
public void testRepairWithFullItemAdjustment() throws Exception {
- invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
-
final LocalDate today = new LocalDate(2013, 7, 19);
final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
@@ -740,7 +517,6 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
new ExpectedInvoiceItemCheck(new LocalDate(2013, 8, 18), new LocalDate(2014, 8, 18), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
// Move clock to 2013-09-17
clock.addDays(30);
busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.INVOICE_ADJUSTMENT);
@@ -755,8 +531,6 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
new ExpectedInvoiceItemCheck(new LocalDate(2013, 9, 17), new LocalDate(2013, 9, 17), InvoiceItemType.CBA_ADJ, new BigDecimal("2202.69")));
invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
-
//
// ITEM ADJUSTMENT PRIOR TO DOING THE REPAIR
//
@@ -789,8 +563,6 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
@Test(groups = "slow")
public void testRepairWithPartialItemAdjustment() throws Exception {
- invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
-
final LocalDate today = new LocalDate(2013, 7, 19);
final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
@@ -821,7 +593,6 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
new ExpectedInvoiceItemCheck(new LocalDate(2013, 8, 18), new LocalDate(2014, 8, 18), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
// Move clock to 2013-09-17
clock.addDays(30);
busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.INVOICE_ADJUSTMENT);
@@ -836,8 +607,6 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
new ExpectedInvoiceItemCheck(new LocalDate(2013, 9, 17), new LocalDate(2013, 9, 17), InvoiceItemType.CBA_ADJ, new BigDecimal("2202.69")));
invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
-
//
// ITEM ADJUSTMENT PRIOR TO DOING THE REPAIR
//
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/util/InvoiceChecker.java b/beatrix/src/test/java/com/ning/billing/beatrix/util/InvoiceChecker.java
index 7925efe..bc4f042 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/util/InvoiceChecker.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/util/InvoiceChecker.java
@@ -20,6 +20,8 @@ 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;
@@ -39,7 +41,9 @@ import com.ning.billing.invoice.api.InvoiceUserApi;
import com.ning.billing.subscription.api.SubscriptionBase;
import com.ning.billing.util.callcontext.CallContext;
+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;
@@ -92,7 +96,34 @@ public class InvoiceChecker {
final List<InvoiceItem> actual = invoice.getInvoiceItems();
Assert.assertEquals(actual.size(), expected.size());
for (final ExpectedInvoiceItemCheck cur : expected) {
+
boolean found = false;
+
+ // First try to find exact match; this is necessary because the for loop below might encounter a similar item -- for instance
+ // same type, same dates, but different amount and choke.
+ final InvoiceItem foundItem = Iterables.tryFind(actual, new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ if (input.getInvoiceItemType() != cur.getType() || (cur.shouldCheckDates() && input.getStartDate().compareTo(cur.getStartDate()) != 0)) {
+ return false;
+ }
+ if (input.getAmount().compareTo(cur.getAmount()) != 0) {
+ return false;
+ }
+
+ if (!cur.shouldCheckDates() ||
+ (cur.getEndDate() == null && input.getEndDate() == null) ||
+ (cur.getEndDate() != null && input.getEndDate() != null && cur.getEndDate().compareTo(input.getEndDate()) == 0)) {
+ return true;
+ }
+ return false;
+ }
+ }).orNull();
+ if (foundItem != null) {
+ continue;
+ }
+
+ // If we could not find it, we still loop again, so that error message helps to debug when there is a 'similar' item.
for (final InvoiceItem in : actual) {
// Match first on type and start date
if (in.getInvoiceItemType() != cur.getType() || (cur.shouldCheckDates() && in.getStartDate().compareTo(cur.getStartDate()) != 0)) {
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 57bd5dc..74a58bb 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
@@ -18,9 +18,6 @@ package com.ning.billing.invoice.generator;
import java.math.BigDecimal;
import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
@@ -28,7 +25,6 @@ import java.util.UUID;
import javax.annotation.Nullable;
-import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.joda.time.Months;
import org.slf4j.Logger;
@@ -41,7 +37,6 @@ import com.ning.billing.clock.Clock;
import com.ning.billing.invoice.api.Invoice;
import com.ning.billing.invoice.api.InvoiceApiException;
import com.ning.billing.invoice.api.InvoiceItem;
-import com.ning.billing.invoice.api.InvoiceItemType;
import com.ning.billing.invoice.model.BillingMode;
import com.ning.billing.invoice.model.DefaultInvoice;
import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
@@ -49,48 +44,15 @@ import com.ning.billing.invoice.model.InAdvanceBillingMode;
import com.ning.billing.invoice.model.InvalidDateSequenceException;
import com.ning.billing.invoice.model.RecurringInvoiceItem;
import com.ning.billing.invoice.model.RecurringInvoiceItemData;
-import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
+import com.ning.billing.invoice.tree.AccountItemTree;
import com.ning.billing.junction.BillingEvent;
import com.ning.billing.junction.BillingEventSet;
import com.ning.billing.junction.BillingModeType;
import com.ning.billing.util.config.InvoiceConfig;
import com.ning.billing.util.currency.KillBillMoney;
-import com.google.common.base.Objects;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.Lists;
import com.google.inject.Inject;
-/**
- * Terminology for repair scenarii:
- * <p/>
- * - A 'repaired' item is an item that was generated and that needs to be repaired because the plan changed for that subscription on that period of time
- * - The 'repair' item is the item that cancels the (to be) repaired item; the repair item amount might not match the (to be) repaired item because:
- * * the (to be) repaired item was already adjusted so we will only repair what is left
- * * in case of partial repair we only repair the part that is not used
- * - The 'reparee' item is only present on disk-- in the existing item list -- in case of full repair; in that case it represents the portion of the item that should still
- * be invoiced for the plan of the repaired item. In case of partial repair it is merged with the repair item and does not exist except as a virtual item in the proposed list
- * <p/>
- * <p/>
- * <p/>
- * Example. We had a 20 subscription for a given period; we charged that amount and later discovered that only 3/4 of the time period were used after which the subscription was cancelled (immediate canellation)
- * <p/>
- * Full repair logic:
- * <p/>
- * Invoice 1: Invoice 2:
- * +20 (repaired) +5 (reparee)
- * -20 (repair)
- * <p/>
- * Partial repair logic:
- * <p/>
- * Invoice 1: Invoice 2: (N/A)
- * +20 (repaired)
- * -15 (repair)
- * <p/>
- * The current version of the code uses partial repair logic but is able to deal with 'full repair' scenarii.
- */
-
public class DefaultInvoiceGenerator implements InvoiceGenerator {
private static final Logger log = LoggerFactory.getLogger(DefaultInvoiceGenerator.class);
@@ -118,14 +80,15 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
validateTargetDate(targetDate);
- final List<InvoiceItem> existingItems = new ArrayList<InvoiceItem>();
+ final AccountItemTree tree = new AccountItemTree(accountId);
+
if (existingInvoices != null) {
for (final Invoice invoice : existingInvoices) {
for (final InvoiceItem item : invoice.getInvoiceItems()) {
if (item.getSubscriptionId() == null || // Always include migration invoices, credits, external charges etc.
!events.getSubscriptionIdsWithAutoInvoiceOff()
.contains(item.getSubscriptionId())) { //don't add items with auto_invoice_off tag
- existingItems.add(item);
+ tree.addExistingItem(item);
}
}
}
@@ -139,191 +102,11 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
// Generate list of proposed invoice items based on billing events from junction-- proposed items are ALL items since beginning of time
final List<InvoiceItem> proposedItems = generateInvoiceItems(invoiceId, accountId, events, adjustedTargetDate, targetCurrency);
- // Remove repaired and repair items -- since they never change and can't be regenerated
- removeRepairedAndRepairInvoiceItems(existingItems, proposedItems);
-
- // Remove from both lists the items in common
- removeMatchingInvoiceItems(existingItems, proposedItems);
-
- // We don't want the Fixed items to be repaired -- as they are setup fees that should be paid
- removeRemainingFixedItemsFromExisting(existingItems);
-
- // Add repair items based on what is left in existing items
- addRepairItems(existingItems, proposedItems);
-
- // Finally add this new items on the new invoice
- invoice.addInvoiceItems(proposedItems);
-
- return proposedItems.size() != 0 ? invoice : null;
- }
-
- private void removeRemainingFixedItemsFromExisting(final List<InvoiceItem> existingItems) {
- final Iterator<InvoiceItem> it = existingItems.iterator();
- while (it.hasNext()) {
- final InvoiceItem cur = it.next();
- if (cur.getInvoiceItemType() == InvoiceItemType.FIXED) {
- it.remove();
- }
- }
- }
-
- /**
- * At this point either we have 0 existingItem left or those left need to be repaired
- *
- * @param existingItems the list of remaining existing items
- * @param proposedItems the list of remaining proposed items
- */
- void addRepairItems(final List<InvoiceItem> existingItems, final List<InvoiceItem> proposedItems) {
- for (final InvoiceItem existingItem : existingItems) {
- if (existingItem.getInvoiceItemType() == InvoiceItemType.RECURRING ||
- existingItem.getInvoiceItemType() == InvoiceItemType.FIXED) {
- final BigDecimal existingAdjustedPositiveAmount = getAdjustedPositiveAmount(existingItems, existingItem.getId());
- final BigDecimal amountNegated = existingItem.getAmount() == null ? null : existingItem.getAmount().subtract(existingAdjustedPositiveAmount).negate();
- if (amountNegated != null && amountNegated.compareTo(BigDecimal.ZERO) < 0) {
- final RepairAdjInvoiceItem candidateRepairItem = new RepairAdjInvoiceItem(existingItem.getInvoiceId(), existingItem.getAccountId(), existingItem.getStartDate(), existingItem.getEndDate(), amountNegated, existingItem.getCurrency(), existingItem.getId());
- addRepairsForItem(existingItem, candidateRepairItem, proposedItems);
- }
- }
- }
- }
-
- /**
- * Add the repair item for the (yet to be) repairedItem. It will merge the candidateRepairItem with reparee item
- *
- * @param repairedItem the item being repaired
- * @param candidateRepairItem the repair item we would have if we were to repair the full period
- * @param proposedItems the list of proposed items
- */
- void addRepairsForItem(final InvoiceItem repairedItem, final RepairAdjInvoiceItem candidateRepairItem, final List<InvoiceItem> proposedItems) {
-
- int nbTotalRepaireeDays = 0;
-
- // totalRepareeItemAmount is negative and represents the portion left after we removed the adjustments for the total period for all the reparees combined
- BigDecimal totalRepareeItemAmount = candidateRepairItem.getAmount();
- final List<InvoiceItem> reparees = new ArrayList<InvoiceItem>();
- for (final InvoiceItem cur : proposedItems) {
- if (isRepareeItemForRepairedItem(repairedItem, cur)) {
- nbTotalRepaireeDays += Days.daysBetween(cur.getStartDate(), cur.getEndDate()).getDays();
- reparees.add(cur);
- totalRepareeItemAmount = totalRepareeItemAmount.add(cur.getAmount());
- }
- }
- int nbTotalRepairedDays = Days.daysBetween(candidateRepairItem.getStartDate(), candidateRepairItem.getEndDate()).getDays() - nbTotalRepaireeDays;
-
- // If we repaired the full period there is no repairee item
- if (reparees.size() == 0) {
- proposedItems.add(candidateRepairItem);
- return;
- }
-
- // Sort the reparees based on startDate in order to create the repair items -- based on the endDate (previous repairee) -> startDate (next reparee)
- Collections.sort(reparees, new Comparator<InvoiceItem>() {
- @Override
- public int compare(final InvoiceItem o1, final InvoiceItem o2) {
- return o1.getStartDate().compareTo(o2.getStartDate());
- }
- });
-
- //Build the repaired
- BigDecimal totalRepairItemAmount = BigDecimal.ZERO;
- List<InvoiceItem> repairedItems = new ArrayList<InvoiceItem>();
- InvoiceItem prevReparee = null;
- final Iterator<InvoiceItem> it = reparees.iterator();
- while (it.hasNext()) {
- final InvoiceItem nextReparee = it.next();
- if (prevReparee != null || nextReparee.getStartDate().compareTo(repairedItem.getStartDate()) > 0) {
- final LocalDate repairedStartDate = (prevReparee == null) ? repairedItem.getStartDate() : prevReparee.getEndDate();
- final LocalDate repairedEndDateDate = nextReparee.getStartDate();
- // repairItemAmount is an approximation of the exact amount by simply prorating totalRepareeItemAmount in the repair period; we make sure last item is calculated based
- // on what is left so the sum of all repairs amount is exactly correct
- final BigDecimal repairItemAmount = (nextReparee.getEndDate().compareTo(candidateRepairItem.getEndDate()) != 0) ?
- InvoiceDateUtils.calculateProrationBetweenDates(repairedStartDate, repairedEndDateDate, nbTotalRepairedDays).multiply(totalRepareeItemAmount) :
- totalRepareeItemAmount.subtract(totalRepairItemAmount);
- totalRepairItemAmount = totalRepairItemAmount.add(repairItemAmount);
- final RepairAdjInvoiceItem repairItem = new RepairAdjInvoiceItem(candidateRepairItem.getInvoiceId(), candidateRepairItem.getAccountId(), repairedStartDate, repairedEndDateDate, repairItemAmount, candidateRepairItem.getCurrency(), repairedItem.getId());
- repairedItems.add(repairItem);
- }
- prevReparee = nextReparee;
- }
-
- // In case we end up with a repair up to the service endDate we need to add this extra item-- this is the 'classic' case with one repairee/repair item
- if (prevReparee.getEndDate().compareTo(candidateRepairItem.getEndDate()) != 0) {
- final BigDecimal repairItemAmount = totalRepareeItemAmount.subtract(totalRepairItemAmount);
- final RepairAdjInvoiceItem repairItem = new RepairAdjInvoiceItem(candidateRepairItem.getInvoiceId(), candidateRepairItem.getAccountId(), prevReparee.getEndDate(), candidateRepairItem.getEndDate(), repairItemAmount, candidateRepairItem.getCurrency(), repairedItem.getId());
- repairedItems.add(repairItem);
- }
-
- // Finally remove all reparees from the proposed items and add all repaired items in the invoice
- for (InvoiceItem reparee : reparees) {
- proposedItems.remove(reparee);
- }
- proposedItems.addAll(repairedItems);
- }
+ tree.mergeWithProposedItems(proposedItems);
+ final List<InvoiceItem> finalItems = tree.getResultingItemList();
+ invoice.addInvoiceItems(finalItems);
- /**
- * Check whether or not the invoiceItem passed is the reparee for that repaired invoice item
- *
- * @param repairedInvoiceItem the repaired invoice item
- * @param invoiceItem any invoice item to compare to
- * @return true if invoiceItem is the reparee for that repaired invoice item
- */
- 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()) &&
- // service period for reparee should be included in service period of repaired-- true for startDate and endDate
- repairedInvoiceItem.getStartDate().compareTo(invoiceItem.getStartDate()) <= 0 &&
- // 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 &&
- repairedInvoiceItem.getEndDate().compareTo(invoiceItem.getEndDate()) >= 0)) &&
- // 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;
- }
-
- /**
- * Check whether the repair invoice item overlaps the proposed item
- *
- * @param repairInvoiceItem the repair invoice item associated to the repaired item for which we pass the subscriptionId
- * @param repairedSubscriptionId the subscriptionId for which this repair points to
- * @param invoiceItem an invoice item to compare to
- * @return
- */
- boolean isRepareeIncludedInRepair(final InvoiceItem repairInvoiceItem, final UUID repairedSubscriptionId, final InvoiceItem invoiceItem) {
- return invoiceItem.getSubscriptionId().equals(repairedSubscriptionId) &&
- repairInvoiceItem.getStartDate().compareTo(invoiceItem.getStartDate()) <= 0 &&
- (invoiceItem.getEndDate() != null &&
- repairInvoiceItem.getEndDate().compareTo(invoiceItem.getEndDate()) >= 0);
- }
-
- // We check to see if there are any adjustments that point to the item we are trying to repair
- // If we did any CREDIT_ADJ or REFUND_ADJ, then we unfortunately we can't know what is the intent
- // was as it applies to the full Invoice, so we ignore it. That might result in an extra positive CBA
- // that would have to be corrected manually. This is the best we can do, and administrators should always
- // use ITEM_ADJUSTMENT rather than CREDIT_ADJ or REFUND_ADJ when possible.
- //
- BigDecimal getAdjustedPositiveAmount(final List<InvoiceItem> existingItems, final UUID linkedItemId) {
- BigDecimal totalAdjustedOnItem = BigDecimal.ZERO;
- final Collection<InvoiceItem> invoiceItems = Collections2.filter(existingItems, new Predicate<InvoiceItem>() {
- @Override
- public boolean apply(final InvoiceItem item) {
- return item.getInvoiceItemType() == InvoiceItemType.ITEM_ADJ &&
- item.getLinkedItemId() != null && item.getLinkedItemId().equals(linkedItemId);
- }
- });
-
- for (final InvoiceItem invoiceItem : invoiceItems) {
- totalAdjustedOnItem = totalAdjustedOnItem.add(invoiceItem.getAmount());
- }
- return totalAdjustedOnItem.negate();
+ return finalItems.size() != 0 ? invoice : null;
}
private void validateTargetDate(final LocalDate targetDate) throws InvoiceApiException {
@@ -349,104 +132,6 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
return maxDate;
}
- /*
- * Removes all matching items from both submitted collections
- */
- void removeMatchingInvoiceItems(final List<InvoiceItem> existingInvoiceItems,
- final List<InvoiceItem> proposedItems) {
- // We can't just use sets here as order matters (we want to keep duplicated in existingInvoiceItems)
- final Iterator<InvoiceItem> proposedItemIterator = proposedItems.iterator();
- while (proposedItemIterator.hasNext()) {
- final InvoiceItem proposedItem = proposedItemIterator.next();
-
- final Iterator<InvoiceItem> existingItemIterator = existingInvoiceItems.iterator();
- while (existingItemIterator.hasNext()) {
- final InvoiceItem existingItem = existingItemIterator.next();
- if (existingItem.matches(proposedItem)) {
- existingItemIterator.remove();
- proposedItemIterator.remove();
- break;
- }
- }
- }
- }
-
- /**
- * Remove from the existing item list all repaired items-- both repaired and repair
- * If this is a partial repair, we also need to find the reparee from the proposed list
- * and remove it.
- *
- * @param existingItems input list of existing items
- * @param proposedItems input list of proposed item
- */
- void removeRepairedAndRepairInvoiceItems(final List<InvoiceItem> existingItems, final List<InvoiceItem> proposedItems) {
-
- final List<UUID> itemsToRemove = new ArrayList<UUID>();
- List<InvoiceItem> itemsToAdd = Lists.newLinkedList();
-
- for (final InvoiceItem item : existingItems) {
- if (item.getInvoiceItemType() == InvoiceItemType.REPAIR_ADJ) {
-
- // Assign for terminology purpose
- final InvoiceItem repairItem = item;
- final InvoiceItem repairedItem = getRepairedInvoiceItem(repairItem.getLinkedItemId(), existingItems);
- // Always look for reparees; if this is a full repair there may not be any reparee to remove, but
- // if this is a partial repair with an additional invoice item adjustment, this is seen as a full repair
- // and yet there is a reparee to remove
-
- final List<InvoiceItem> removedReparees = removeProposedRepareesForPartialrepair(repairedItem, repairItem, proposedItems);
- itemsToAdd.addAll((computeNonRepairedItems(repairedItem, repairItem, removedReparees)));
- itemsToRemove.add(repairItem.getId());
- itemsToRemove.add(repairItem.getLinkedItemId());
- }
- }
- final Iterator<InvoiceItem> iterator = existingItems.iterator();
- while (iterator.hasNext()) {
- final InvoiceItem item = iterator.next();
- if (itemsToRemove.contains(item.getId())) {
- iterator.remove();
- }
- }
- existingItems.addAll(itemsToAdd);
- }
-
-
- /**
- * Removes the reparee from proposed list of items if it exists.
- *
- * @param repairedItem the repaired item
- * @param proposedItems the list of existing items
- */
- protected List<InvoiceItem> removeProposedRepareesForPartialrepair(final InvoiceItem repairedItem, final InvoiceItem repairItem, final List<InvoiceItem> proposedItems) {
-
- List<InvoiceItem> removedReparees = Collections.emptyList();
- final Iterator<InvoiceItem> it = proposedItems.iterator();
- while (it.hasNext()) {
- final InvoiceItem cur = it.next();
- final UUID repairedSubscriptionId = repairedItem.getSubscriptionId();
- // We remove the item if we already billed for it, that is:
- // - First we check if the current item is a reparee for that repaired
- // - Second we check whether that reparee is outside of the repair period and therefore has already been accounted for. If not we keep it.
- if (isRepareeItemForRepairedItem(repairedItem, cur) && !isRepareeIncludedInRepair(repairItem, repairedSubscriptionId, cur)) {
- if (removedReparees.size() == 0) {
- removedReparees = Lists.newLinkedList();
- }
- removedReparees.add(cur);
- it.remove();
- }
- }
- return removedReparees;
- }
-
- private InvoiceItem getRepairedInvoiceItem(final UUID repairedInvoiceItemId, final List<InvoiceItem> existingItems) {
- for (InvoiceItem cur : existingItems) {
- if (cur.getId().equals(repairedInvoiceItemId)) {
- return cur;
- }
- }
- throw new IllegalStateException("Cannot find repaired invoice item " + repairedInvoiceItemId);
- }
-
private List<InvoiceItem> generateInvoiceItems(final UUID invoiceId, final UUID accountId, final BillingEventSet events,
final LocalDate targetDate, final Currency currency) throws InvoiceApiException {
final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
@@ -540,71 +225,6 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
return items;
}
- /**
- *
- * It compares the full period of the repairedItem with the list of repairees and repair
- *
- * @param repairedItem the repair item
- * @param repairItem (one of) the repair item pointing to the repairedItem
- * @param removedReparees the reparees from propsoed list that were found matching that repairedItem
- * @return
- */
- List<InvoiceItem> computeNonRepairedItems(final InvoiceItem repairedItem, final InvoiceItem repairItem, final List<InvoiceItem> removedReparees) {
-
- final List<InvoiceItem> result = new LinkedList<InvoiceItem>();
- if (removedReparees.size() == 0 || repairedItem.getInvoiceItemType() != InvoiceItemType.RECURRING) {
- return result;
- }
-
- final List<InvoiceItem> repairAndReparees = new ArrayList<InvoiceItem>(removedReparees);
- repairAndReparees.add(repairItem);
-
- Collections.sort(repairAndReparees, new Comparator<InvoiceItem>() {
- @Override
- public int compare(final InvoiceItem o1, final InvoiceItem o2) {
- return o1.getStartDate().compareTo(o2.getStartDate());
- }
- });
-
- int nbTotalRepairedDays = Days.daysBetween(repairedItem.getStartDate(), repairedItem.getEndDate()).getDays();
-
- LocalDate prevEnd = null;
- final LocalDate startDate = repairedItem.getStartDate();
- for (InvoiceItem cur : repairAndReparees) {
- if (prevEnd == null) {
- if (cur.getStartDate().compareTo(startDate) > 0) {
- result.add(createRecurringInvoiceItemForRepair(repairedItem.getStartDate(), cur.getStartDate(), repairedItem, nbTotalRepairedDays));
- }
- } else {
- if (prevEnd.compareTo(cur.getStartDate()) < 0) {
- result.add(createRecurringInvoiceItemForRepair(prevEnd, cur.getStartDate(), repairedItem, nbTotalRepairedDays));
- }
- }
- prevEnd = cur.getEndDate();
- }
-
- if (prevEnd.compareTo(repairedItem.getEndDate()) < 0) {
- result.add(createRecurringInvoiceItemForRepair(prevEnd, repairedItem.getEndDate(), repairedItem, nbTotalRepairedDays));
- }
- return result;
- }
-
- private InvoiceItem createRecurringInvoiceItemForRepair(final LocalDate startDate, final LocalDate endDate, final InvoiceItem repairedItem, final int nbTotalRepairedDays) {
- final BigDecimal amount = InvoiceDateUtils.calculateProrationBetweenDates(startDate, endDate, nbTotalRepairedDays).multiply(repairedItem.getRate());
- return new RecurringInvoiceItem(repairedItem.getInvoiceId(),
- repairedItem.getAccountId(),
- repairedItem.getBundleId(),
- repairedItem.getSubscriptionId(),
- repairedItem.getPlanName(),
- repairedItem.getPhaseName(),
- startDate,
- endDate,
- amount,
- repairedItem.getRate(),
- repairedItem.getCurrency());
-
- }
-
private BillingMode instantiateBillingMode(final BillingModeType billingMode) {
switch (billingMode) {
case IN_ADVANCE:
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java b/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java
new file mode 100644
index 0000000..8aaa7e7
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
+/**
+ * Tree of invoice items for a given account.
+ *
+ * <p>It contains a map of <tt>SubscriptionItemTree</tt> and the logic is executed independently for all items
+ * associated to a given subscription. That also means that invoice item adjustment which cross subscriptions
+ * can't be correctly handled when they compete with other forms of adjustments.
+ *
+ * <p>The class is not thread safe, there is no such use case today, and there is a lifecyle to respect:
+ * <ul>
+ * <li>Add existing invoice items
+ * <li>Build the tree,
+ * <li>Merge the proposed list
+ * <li>Retrieves final list
+ * <ul/>
+ */
+public class AccountItemTree {
+
+ private final UUID accountId;
+ private final Map<UUID, SubscriptionItemTree> subscriptionItemTree;
+ private final List<InvoiceItem> allExistingItems;
+
+ private boolean isBuilt;
+
+ public AccountItemTree(final UUID accountId) {
+ this.accountId = accountId;
+ this.subscriptionItemTree = new HashMap<UUID, SubscriptionItemTree>();
+ this.isBuilt = false;
+ this.allExistingItems = new LinkedList<InvoiceItem>();
+ }
+
+ /**
+ * build the subscription trees after they have been populated with existing items on disk
+ */
+ public void build() {
+ Preconditions.checkState(!isBuilt);
+ for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+ tree.build();
+ }
+ isBuilt = true;
+ }
+ /**
+ * Populate tree from existing items on disk
+ *
+ * @param existingItem an item read on disk
+ */
+ public void addExistingItem(final InvoiceItem existingItem) {
+
+ Preconditions.checkState(!isBuilt);
+ if (existingItem.getInvoiceItemType() != InvoiceItemType.RECURRING &&
+ existingItem.getInvoiceItemType() != InvoiceItemType.REPAIR_ADJ &&
+ existingItem.getInvoiceItemType() != InvoiceItemType.FIXED &&
+ existingItem.getInvoiceItemType() != InvoiceItemType.ITEM_ADJ) {
+ return;
+ }
+
+ allExistingItems.add(existingItem);
+
+ final UUID subscriptionId = getSubscriptionId(existingItem, allExistingItems);
+ Preconditions.checkNotNull(subscriptionId);
+
+ if (!subscriptionItemTree.containsKey(subscriptionId)) {
+ subscriptionItemTree.put(subscriptionId, new SubscriptionItemTree(subscriptionId));
+ }
+ final SubscriptionItemTree tree = subscriptionItemTree.get(subscriptionId);
+ tree.addItem(existingItem);
+ }
+
+ /**
+ * Rebuild the new tree by merging current on-disk existing view with new proposed list.
+ *
+ * @param proposedItems list of proposed item that should be merged with current existing view
+ */
+ public void mergeWithProposedItems(final List<InvoiceItem> proposedItems) {
+
+ for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+ tree.flatten(true);
+ }
+ isBuilt = true;
+
+ for (InvoiceItem item : proposedItems) {
+ final UUID subscriptionId = getSubscriptionId(item, null);
+ SubscriptionItemTree tree = subscriptionItemTree.get(subscriptionId);
+ if (tree == null) {
+ tree = new SubscriptionItemTree(subscriptionId);
+ subscriptionItemTree.put(subscriptionId, tree);
+ }
+ tree.mergeProposedItem(item);
+ }
+
+ for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+ tree.buildForMerge();
+ }
+ }
+
+ /**
+ *
+ * @return the resulting list of items that should be written to disk
+ */
+ public List<InvoiceItem> getResultingItemList() {
+ final List<InvoiceItem> result = new ArrayList<InvoiceItem>();
+ for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+ final List<InvoiceItem> simplifiedView = tree.getView();
+ if (simplifiedView.size() > 0) {
+ result.addAll(simplifiedView);
+ }
+ }
+ return result;
+ }
+
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ private UUID getSubscriptionId(final InvoiceItem item, final List<InvoiceItem> allItems) {
+ if (item.getInvoiceItemType() == InvoiceItemType.RECURRING ||
+ item.getInvoiceItemType() == InvoiceItemType.FIXED) {
+ return item.getSubscriptionId();
+ } else {
+ final InvoiceItem linkedItem = Iterables.tryFind(allItems, new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ return item.getLinkedItemId().equals(input.getId());
+ }
+ }).get();
+ return linkedItem.getSubscriptionId();
+ }
+ }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/Item.java b/invoice/src/main/java/com/ning/billing/invoice/tree/Item.java
new file mode 100644
index 0000000..40e97ee
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/Item.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.Days;
+import org.joda.time.LocalDate;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.generator.InvoiceDateUtils;
+import com.ning.billing.invoice.model.RecurringInvoiceItem;
+import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
+import com.ning.billing.util.currency.KillBillMoney;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+
+/**
+ * An generic invoice item that contains all pertinent fields regarding of its InvoiceItemType.
+ * <p/>
+ * It contains an action that determines what to do when building the tree (whether in normal or merge mode). It also
+ * keeps track of current adjusted and repair amount so subsequent repair can be limited to what is left.
+ */
+public class Item {
+
+ private final UUID id;
+ private final UUID accountId;
+ private final UUID bundleId;
+ private final UUID subscriptionId;
+ private final UUID invoiceId;
+ private final String planName;
+ private final String phaseName;
+ private final LocalDate startDate;
+ private final LocalDate endDate;
+ private final BigDecimal amount;
+ private final BigDecimal rate;
+ private final Currency currency;
+ private final DateTime createdDate;
+ private final UUID linkedId;
+
+ private BigDecimal currentRepairedAmount;
+ private BigDecimal adjustedAmount;
+
+ private final ItemAction action;
+
+ public enum ItemAction {
+ ADD,
+ CANCEL
+ }
+
+ public Item(final Item item, final ItemAction action) {
+ this.id = item.id;
+ this.accountId = item.accountId;
+ this.bundleId = item.bundleId;
+ this.subscriptionId = item.subscriptionId;
+ this.invoiceId = item.invoiceId;
+ this.planName = item.planName;
+ this.phaseName = item.phaseName;
+ this.startDate = item.startDate;
+ this.endDate = item.endDate;
+ this.amount = item.amount;
+ this.rate = item.rate;
+ this.currency = item.currency;
+ // In merge mode, the reverse item needs to correctly point to itself (repair of original item)
+ this.linkedId = action == ItemAction.ADD ? item.linkedId : this.id;
+ this.createdDate = item.createdDate;
+ this.currentRepairedAmount = item.currentRepairedAmount;
+ this.adjustedAmount = item.adjustedAmount;
+
+ this.action = action;
+ }
+
+ public Item(final InvoiceItem item, final ItemAction action) {
+ this.id = item.getId();
+ this.accountId = item.getAccountId();
+ this.bundleId = item.getBundleId();
+ this.subscriptionId = item.getSubscriptionId();
+ this.invoiceId = item.getInvoiceId();
+ this.planName = item.getPlanName();
+ this.phaseName = item.getPhaseName();
+ this.startDate = item.getStartDate();
+ this.endDate = item.getEndDate();
+ this.amount = item.getAmount().abs();
+ this.rate = item.getRate();
+ this.currency = item.getCurrency();
+ this.linkedId = item.getLinkedItemId();
+ this.createdDate = item.getCreatedDate();
+ this.action = action;
+
+ this.currentRepairedAmount = BigDecimal.ZERO;
+ this.adjustedAmount = BigDecimal.ZERO;
+ }
+
+ public InvoiceItem toInvoiceItem() {
+ return toProratedInvoiceItem(startDate, endDate);
+ }
+
+ public InvoiceItem toProratedInvoiceItem(final LocalDate newStartDate, final LocalDate newEndDate) {
+
+ int nbTotalDays = Days.daysBetween(startDate, endDate).getDays();
+ final boolean prorated = !(newStartDate.compareTo(startDate) == 0 && newEndDate.compareTo(endDate) == 0);
+
+ // Pro-ration is built by using the startDate, endDate and amount of this item instead of using the rate and a potential full period.
+ final BigDecimal positiveAmount = prorated ? InvoiceDateUtils.calculateProrationBetweenDates(newStartDate, newEndDate, nbTotalDays)
+ .multiply(amount) : amount;
+
+ if (action == ItemAction.ADD) {
+ return new RecurringInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, newStartDate, newEndDate, KillBillMoney.of(positiveAmount, currency), rate, currency);
+ } else {
+ // We first compute the maximum amount after adjustment and that sets the amount limit of how much can be repaired.
+ final BigDecimal maxAvailableAmountAfterAdj = amount.subtract(adjustedAmount);
+ final BigDecimal maxAvailableAmountForRepair = maxAvailableAmountAfterAdj.subtract(currentRepairedAmount);
+ final BigDecimal positiveAmountForRepair = positiveAmount.compareTo(maxAvailableAmountForRepair) <= 0 ? positiveAmount : maxAvailableAmountForRepair;
+ return positiveAmountForRepair.compareTo(BigDecimal.ZERO) > 0 ? new RepairAdjInvoiceItem(invoiceId, accountId, newStartDate, newEndDate, KillBillMoney.of(positiveAmountForRepair.negate(), currency), currency, linkedId) : null;
+ }
+ }
+
+ public void incrementAdjustedAmount(final BigDecimal increment) {
+ Preconditions.checkState(increment.compareTo(BigDecimal.ZERO) > 0);
+ adjustedAmount = adjustedAmount.add(increment);
+ }
+
+ public void incrementCurrentRepairedAmount(final BigDecimal increment) {
+ Preconditions.checkState(increment.compareTo(BigDecimal.ZERO) > 0);
+ currentRepairedAmount = currentRepairedAmount.add(increment);
+ }
+
+ public ItemAction getAction() {
+ return action;
+ }
+
+ public UUID getLinkedId() {
+ return linkedId;
+ }
+
+ public LocalDate getEndDate() {
+ return endDate;
+ }
+
+ public LocalDate getStartDate() {
+ return startDate;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ /**
+ * Compare two items to check whether there are the same kind; that is whether or not they build for the same product/plan.
+ *
+ * @param other item to compare with
+ * @return
+ */
+ public boolean isSameKind(final Item other) {
+
+ final InvoiceItem otherItem = other.toInvoiceItem();
+
+ return !id.equals(otherItem.getId()) &&
+ // Finally, for the tricky part... In case of complete repairs, the new invoiceItem 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(planName, "").equals(Objects.firstNonNull(otherItem.getPlanName(), "")) &&
+ Objects.firstNonNull(phaseName, "").equals(Objects.firstNonNull(otherItem.getPhaseName(), "")) &&
+ Objects.firstNonNull(rate, BigDecimal.ZERO).compareTo(Objects.firstNonNull(otherItem.getRate(), BigDecimal.ZERO)) == 0;
+ }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsInterval.java b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsInterval.java
new file mode 100644
index 0000000..fa7bf04
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsInterval.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import com.ning.billing.invoice.tree.Item.ItemAction;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+/**
+ * Keeps track of all the items existing on a specified interval.
+ */
+public class ItemsInterval {
+
+ private final NodeInterval interval;
+ private LinkedList<Item> items;
+
+ public ItemsInterval(final NodeInterval interval) {
+ this(interval, null);
+ }
+
+ public ItemsInterval(final NodeInterval interval, final Item initialItem) {
+ this.interval = interval;
+ this.items = Lists.newLinkedList();
+ if (initialItem != null) {
+ items.add(initialItem);
+ }
+ }
+
+ public boolean containsItem(final UUID targetId) {
+ return Iterables.tryFind(items, new Predicate<Item>() {
+ @Override
+ public boolean apply(final Item input) {
+ return input.getId().equals(targetId);
+ }
+ }).orNull() != null;
+ }
+
+ public void setAdjustment(final BigDecimal amount, final UUID targetId) {
+ final Item item = Iterables.tryFind(items, new Predicate<Item>() {
+ @Override
+ public boolean apply(final Item input) {
+ return input.getId().equals(targetId);
+ }
+ }).get();
+ item.incrementAdjustedAmount(amount);
+ }
+
+ public List<Item> getItems() {
+ return items;
+ }
+
+ public void buildForMissingInterval(final LocalDate startDate, final LocalDate endDate, final List<Item> output, final boolean addRepair) {
+ final Item item = createNewItem(startDate, endDate, addRepair);
+ if (item != null) {
+ output.add(item);
+ }
+ }
+
+ /**
+ * Determines what is left based on the mergeMode and the action for each item.
+ *
+ * @param output
+ * @param mergeMode
+ */
+ public void buildFromItems(final List<Item> output, final boolean mergeMode) {
+
+ final Set<UUID> repairedIds = new HashSet<UUID>();
+ final ListIterator<Item> it = items.listIterator(items.size());
+
+ while (it.hasPrevious()) {
+ final Item cur = it.previous();
+ switch (cur.getAction()) {
+ case ADD:
+ // Don't consider ADD items in mergeMode as they are only there to specify the bounderies of the repair elements.
+ if (!mergeMode) {
+ // If we found a CANCEL item pointing to that item then don't return it as it was repair (full repair scenario)
+ if (!repairedIds.contains(cur.getId())) {
+ output.add(cur);
+ }
+ }
+ break;
+
+ case CANCEL:
+ // In merge logic we want to CANCEL (repair) items)
+ if (mergeMode) {
+ output.add(cur);
+ }
+ // In all cases populate the set with the id of target item being repaired
+ if (cur.getLinkedId() != null) {
+ repairedIds.add(cur.getLinkedId());
+ }
+ break;
+ }
+ }
+ }
+
+ // Just ensure that ADD items precedes CANCEL items
+ public void insertSortedItem(final Item item) {
+ items.add(item);
+ Collections.sort(items, new Comparator<Item>() {
+ @Override
+ public int compare(final Item o1, final Item o2) {
+ if (o1.getAction() == ItemAction.ADD && o2.getAction() == ItemAction.CANCEL) {
+ return -1;
+ } else if (o1.getAction() == ItemAction.CANCEL && o2.getAction() == ItemAction.ADD) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ });
+ }
+
+ public void cancelItems(final Item item) {
+ Preconditions.checkState(item.getAction() == ItemAction.ADD);
+ Preconditions.checkState(items.size() == 1);
+ Preconditions.checkState(items.get(0).getAction() == ItemAction.CANCEL);
+ items.clear();
+ }
+
+ /**
+ * Creates a new item.
+ * <p/>
+ * <ul>
+ * <li>In normal mode, we only consider ADD items. This happens when for instance an existing item was partially repaired
+ * and there is a need to create a new item which represents the part left -- that was not repaired.
+ * <li>In mergeMode, we allow to create new items that are the missing repaired items (CANCEL).
+ * </ul>
+ *
+ * @param startDate start date of the new item to create
+ * @param endDate end date of the new item to create
+ * @param mergeMode mode to consider.
+ * @return
+ */
+ private Item createNewItem(LocalDate startDate, LocalDate endDate, final boolean mergeMode) {
+
+ final List<Item> itemToConsider = new LinkedList<Item>();
+ buildFromItems(itemToConsider, mergeMode);
+ if (itemToConsider.size() == 0) {
+ return null;
+ }
+
+ Preconditions.checkState(itemToConsider.size() == 1);
+ final Item item = itemToConsider.size() == 1 ? itemToConsider.get(0) : null;
+ Preconditions.checkState((!mergeMode && item.getAction() == ItemAction.ADD) ||
+ (mergeMode && item.getAction() == ItemAction.CANCEL));
+
+ final Item result = new Item(item.toProratedInvoiceItem(startDate, endDate), item.getAction());
+ if (item.getAction() == ItemAction.CANCEL && result != null) {
+ item.incrementCurrentRepairedAmount(result.getAmount());
+ }
+ return result;
+ }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java b/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
new file mode 100644
index 0000000..de7fd1b
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+
+public class NodeInterval {
+
+ private LocalDate start;
+ private LocalDate end;
+ private ItemsInterval items;
+
+ private NodeInterval parent;
+ private NodeInterval leftChild;
+ private NodeInterval rightSibling;
+
+ public NodeInterval() {
+ this.items = new ItemsInterval(this);
+ }
+
+ public NodeInterval(final NodeInterval parent, final Item item) {
+ this.start = item.getStartDate();
+ this.end = item.getEndDate();
+ this.items = new ItemsInterval(this, item);
+ this.parent = parent;
+ this.leftChild = null;
+ this.rightSibling = null;
+ }
+
+ /**
+ * Build the output list from the elements in the tree.
+ * <p/>
+ * In the simple mode, mergeMode = false, there is no limit in the depth of the tree,
+ * and the build strategy is to first consider the lowest child for a given period
+ * and go up the tree adding missing interval if needed. For e.g, one of the possible scenario:
+ * <pre>
+ * D1 D2
+ * |---------------------------------------------------| Plan P1
+ * D1' D2'
+ * |---------------|/////////////////////////////| Plan P2, REPAIR
+ *
+ * In that case we will generate:
+ * [D1,D1') on Plan P1; [D1', D2') on Plan P2, and [D2', D2) repair item
+ *
+ * <pre/>
+ *
+ * In the merge mode, the strategy is different, the tree is fairly shallow
+ * and the goal is to generate the repair items; @see mergeProposedItem
+ *
+ * @param output result list of items
+ * @param mergeMode mode used to produce output list
+ */
+ public void build(final List<Item> output, boolean mergeMode) {
+
+ // There is no sub-interval, just add our own items.
+ if (leftChild == null) {
+ items.buildFromItems(output, mergeMode);
+ return;
+ }
+
+ LocalDate curDate = start;
+ NodeInterval curChild = leftChild;
+ while (curChild != null) {
+ if (curChild.getStart().compareTo(curDate) > 0) {
+ items.buildForMissingInterval(curDate, curChild.getStart(), output, mergeMode);
+ }
+ curChild.build(output, mergeMode);
+ curDate = curChild.getEnd();
+ curChild = curChild.getRightSibling();
+ }
+ if (curDate.compareTo(end) < 0) {
+ items.buildForMissingInterval(curDate, end, output, mergeMode);
+ }
+ }
+
+ /**
+ * The merge tree is initially constructed by flattening all the existing items and reversing them (CANCEL node).
+ * That means that if we were to not merge any new proposed items, we would end up with only those reversed existing
+ * items, and they would all end up repaired-- which is what we want.
+ * <p/>
+ * However, if there are new proposed items, then we look to see if they are children one our existing reverse items
+ * so that we can generate the repair pieces missing. For e.g, below is one scenario among so many:
+ * <p/>
+ * <pre>
+ * D1 D2
+ * |---------------------------------------------------| (existing reversed (CANCEL) item
+ * D1' D2'
+ * |---------------| (proposed same plan)
+ * </pre>
+ * In that case we want to generated a repair for [D1, D1') and [D2',D2)
+ * <p/>
+ * Note that this tree is never very deep, only 3 levels max, with exiting at the first level
+ * and proposed that are the for the exact same plan but for different dates below.
+ *
+ * @param newNode a new proposed item
+ * @return true if the item was merged and will trigger a repair or false if the proposed item should be kept as such
+ * and no repair generated.
+ */
+ public boolean mergeProposedItem(final NodeInterval newNode) {
+
+ Preconditions.checkState(newNode.getItems().size() == 1, "Expected new node to have only one item");
+ final Item newNodeItem = newNode.getItems().get(0);
+
+ if (!isRoot() && newNodeItem.getStartDate().compareTo(start) == 0 && newNodeItem.getEndDate().compareTo(end) == 0) {
+ items.cancelItems(newNodeItem);
+ return true;
+ }
+ computeRootInterval(newNode);
+
+ if (leftChild == null) {
+ // There is no existing items, only new proposed one, nothing to add in that merge tree
+ if (isRoot()) {
+ return false;
+ } else {
+ // Proposed item is the first child of an existing item with the same product info.
+ leftChild = newNode;
+ return true;
+
+ }
+ }
+
+ NodeInterval prevChild = null;
+ NodeInterval curChild = leftChild;
+ do {
+ if (curChild.isItemContained(newNodeItem)) {
+ final Item existingNodeItem = curChild.getItems().get(0);
+
+ Preconditions.checkState(curChild.getItems().size() == 1, "Expected existing node to have only one item");
+ if (existingNodeItem.isSameKind(newNodeItem)) {
+ // Proposed item has same product info than parent and is contained so insert it at the right place in the tree
+ curChild.mergeProposedItem(newNode);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // STEPH test for that code path
+ if (newNodeItem.getStartDate().compareTo(curChild.getStart()) < 0) {
+ newNode.rightSibling = curChild;
+ if (prevChild == null) {
+ leftChild = newNode;
+ } else {
+ prevChild.rightSibling = newNode;
+ }
+ return true;
+ }
+
+ prevChild = curChild;
+ curChild = curChild.rightSibling;
+ } while (curChild != null);
+
+ if (isRoot()) {
+ // The new proposed item spans over a new interval, nothing to add in the merge tree
+ return false;
+ } else {
+ prevChild.rightSibling = newNode;
+ return true;
+ }
+ }
+
+ /**
+ * Add an existing item in the tree of items.
+ *
+ * @param newNode new existing item to be added
+ */
+ public void addExistingItem(final NodeInterval newNode) {
+ final Item item = newNode.getItems().get(0);
+ if (!isRoot() && item.getStartDate().compareTo(start) == 0 && item.getEndDate().compareTo(end) == 0) {
+ items.insertSortedItem(item);
+ return;
+ }
+ computeRootInterval(newNode);
+ addNode(newNode);
+ }
+
+ /**
+ * Add the adjustment amount on the item specified by the targetId.
+ *
+ * @param adjustementDate date of the adjustment
+ * @param amount amount of the adjustment
+ * @param targetId item that has been adjusted
+ */
+ public void addAdjustment(final LocalDate adjustementDate, final BigDecimal amount, final UUID targetId) {
+ NodeInterval node = findNode(adjustementDate, targetId);
+ Preconditions.checkNotNull(node, "Cannot add adjustement for item = " + targetId + ", date = " + adjustementDate);
+ node.setAdjustment(amount.negate(), targetId);
+ }
+
+ public boolean isItemContained(final Item item) {
+ return (item.getStartDate().compareTo(start) >= 0 &&
+ item.getStartDate().compareTo(end) <= 0 &&
+ item.getEndDate().compareTo(start) >= 0 &&
+ item.getEndDate().compareTo(end) <= 0);
+ }
+
+ public boolean isItemOverlap(final Item item) {
+ return ((item.getStartDate().compareTo(start) < 0 &&
+ item.getEndDate().compareTo(end) >= 0) ||
+ (item.getStartDate().compareTo(start) <= 0 &&
+ item.getEndDate().compareTo(end) > 0));
+ }
+
+
+
+ public boolean isRoot() {
+ return parent == null;
+ }
+
+ public LocalDate getStart() {
+ return start;
+ }
+
+ public LocalDate getEnd() {
+ return end;
+ }
+
+ public NodeInterval getParent() {
+ return parent;
+ }
+
+ public NodeInterval getLeftChild() {
+ return leftChild;
+ }
+
+ public NodeInterval getRightSibling() {
+ return rightSibling;
+ }
+
+ public List<Item> getItems() {
+ return items.getItems();
+ }
+
+ public boolean containsItem(final UUID targetId) {
+ return items.containsItem(targetId);
+ }
+
+ // STEPH TODO are parents correctly maintained and/or do we need them?
+ private void addNode(final NodeInterval newNode) {
+ final Item item = newNode.getItems().get(0);
+ if (leftChild == null) {
+ leftChild = newNode;
+ return;
+ }
+
+ NodeInterval prevChild = null;
+ NodeInterval curChild = leftChild;
+ do {
+ if (curChild.isItemContained(item)) {
+ curChild.addExistingItem(newNode);
+ return;
+ }
+
+ if (curChild.isItemOverlap(item)) {
+ rebalance(newNode);
+ return;
+ }
+
+ if (item.getStartDate().compareTo(curChild.getStart()) < 0) {
+ newNode.rightSibling = curChild;
+ if (prevChild == null) {
+ leftChild = newNode;
+ } else {
+ prevChild.rightSibling = newNode;
+ }
+ return;
+ }
+ prevChild = curChild;
+ curChild = curChild.rightSibling;
+ } while (curChild != null);
+
+ prevChild.rightSibling = newNode;
+ }
+
+ /**
+ * Since items may be added out of order, there is no guarantee that we don't suddenly had a new node
+ * whose interval emcompasses cuurent node(s). In which case we need to rebalance the tree.
+ *
+ * @param newNode node that triggered a rebalance operation
+ */
+ private void rebalance(final NodeInterval newNode) {
+
+ final Item item = newNode.getItems().get(0);
+
+ NodeInterval prevRebalanced = null;
+ NodeInterval curChild = leftChild;
+ List<NodeInterval> toBeRebalanced = Lists.newLinkedList();
+ do {
+ if (curChild.isItemOverlap(item)) {
+ toBeRebalanced.add(curChild);
+ } else {
+ if (toBeRebalanced.size() > 0) {
+ break;
+ }
+ prevRebalanced = curChild;
+ }
+ curChild = curChild.rightSibling;
+ } while (curChild != null);
+
+ newNode.rightSibling = toBeRebalanced.get(toBeRebalanced.size() - 1).rightSibling;
+ if (prevRebalanced == null) {
+ leftChild = newNode;
+ } else {
+ prevRebalanced.rightSibling = newNode;
+ }
+
+ NodeInterval prev = null;
+ for (NodeInterval cur : toBeRebalanced) {
+ if (prev == null) {
+ newNode.leftChild = cur;
+ } else {
+ prev.rightSibling = cur;
+ }
+ prev = cur;
+ }
+ }
+
+ private void computeRootInterval(final NodeInterval newNode) {
+ if (!isRoot()) {
+ return;
+ }
+ this.start = (start == null || start.compareTo(newNode.getStart()) > 0) ? newNode.getStart() : start;
+ this.end = (end == null || end.compareTo(newNode.getEnd()) < 0) ? newNode.getEnd() : end;
+ }
+
+ private void setAdjustment(final BigDecimal amount, final UUID linkedId) {
+ items.setAdjustment(amount, linkedId);
+ }
+
+ private NodeInterval findNode(final LocalDate date, final UUID targetItemId) {
+ Preconditions.checkState(isRoot(), "findNode can only be called from root");
+ return findNodeRecursively2(this, date, targetItemId);
+ }
+
+ // TODO That method should be use instaed of findNodeRecursively2 to search the node more effectively using the time
+ // but unfortunately that fails because of our test that use the wrong date when doing adjustments.
+ private NodeInterval findNodeRecursively(final NodeInterval curNode, final LocalDate date, final UUID targetItemId) {
+ if (date.compareTo(curNode.getStart()) < 0 || date.compareTo(curNode.getEnd()) > 0) {
+ return null;
+ }
+ NodeInterval curChild = curNode.getLeftChild();
+ while (curChild != null) {
+ if (curChild.getStart().compareTo(date) <= 0 && curChild.getEnd().compareTo(date) >= 0) {
+ if (curChild.containsItem(targetItemId)) {
+ return curChild;
+ } else {
+ return findNodeRecursively(curChild, date, targetItemId);
+ }
+ }
+ curChild = curChild.getRightSibling();
+ }
+ return null;
+ }
+
+ private NodeInterval findNodeRecursively2(final NodeInterval curNode, final LocalDate date, final UUID targetItemId) {
+
+ if (!curNode.isRoot() && curNode.containsItem(targetItemId)) {
+ return curNode;
+ }
+
+ NodeInterval curChild = curNode.getLeftChild();
+ while (curChild != null) {
+ final NodeInterval result = findNodeRecursively2(curChild, date, targetItemId);
+ if (result != null) {
+ return result;
+ }
+ curChild = curChild.getRightSibling();
+ }
+ return null;
+ }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java b/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java
new file mode 100644
index 0000000..49eb390
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.tree.Item.ItemAction;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
+
+/**
+ * Tree of invoice items for a given subscription.
+ */
+public class SubscriptionItemTree {
+
+ private boolean isBuilt;
+
+ private final UUID subscriptionId;
+ private NodeInterval root;
+
+ private List<Item> items;
+
+ private List<InvoiceItem> existingFixedItems;
+ private List<InvoiceItem> remainingFixedItems;
+ private List<InvoiceItem> pendingItemAdj;
+
+ private static final Comparator<InvoiceItem> INVOICE_ITEM_COMPARATOR = new Comparator<InvoiceItem>() {
+ @Override
+ public int compare(final InvoiceItem o1, final InvoiceItem o2) {
+ int startDateComp = o1.getStartDate().compareTo(o2.getStartDate());
+ if (startDateComp != 0) {
+ return startDateComp;
+ }
+ int itemTypeComp = Integer.compare(o1.getInvoiceItemType().ordinal(), o2.getInvoiceItemType().ordinal());
+ if (itemTypeComp != 0) {
+ return itemTypeComp;
+ }
+ Preconditions.checkState(false, "Unexpected list of items for subscription " + o1.getSubscriptionId());
+ // Never reached...
+ return 0;
+ }
+ };
+
+ public SubscriptionItemTree(final UUID subscriptionId) {
+ this.subscriptionId = subscriptionId;
+ this.root = new NodeInterval();
+ this.items = new LinkedList<Item>();
+ this.existingFixedItems = new LinkedList<InvoiceItem>();
+ this.remainingFixedItems = new LinkedList<InvoiceItem>();
+ this.pendingItemAdj = new LinkedList<InvoiceItem>();
+ this.isBuilt = false;
+ }
+
+ /**
+ * Build the tree to return the list of existing items.
+ */
+ public void build() {
+ Preconditions.checkState(!isBuilt);
+ for (InvoiceItem item : pendingItemAdj) {
+ root.addAdjustment(item.getStartDate(), item.getAmount(), item.getLinkedItemId());
+ }
+ pendingItemAdj.clear();
+ root.build(items, false);
+ isBuilt = true;
+ }
+
+ /**
+ * Flattens the tree so its depth only has one levl below root -- becomes a list.
+ * <p>
+ * If the tree was not built, it is first built. The list of items is cleared and the state is now reset to unbuilt.
+ *
+ * @param reverse whether to reverse the existing items (recurring items now show up as CANCEL instead of ADD)
+ */
+ public void flatten(boolean reverse) {
+ if (!isBuilt) {
+ build();
+ }
+ root = new NodeInterval();
+ for (Item item : items) {
+ Preconditions.checkState(item.getAction() == ItemAction.ADD);
+ root.addExistingItem(new NodeInterval(root, new Item(item, reverse ? ItemAction.CANCEL : ItemAction.ADD)));
+ }
+ items.clear();
+ isBuilt = false;
+ }
+
+ public void buildForMerge() {
+ Preconditions.checkState(!isBuilt);
+ root.build(items, true);
+ isBuilt = true;
+ }
+
+ /**
+ * Add an existing item in the tree.
+ *
+ * @param invoiceItem new existing invoice item on disk.
+ */
+ public void addItem(final InvoiceItem invoiceItem) {
+
+ Preconditions.checkState(!isBuilt);
+ switch (invoiceItem.getInvoiceItemType()) {
+ case RECURRING:
+ root.addExistingItem(new NodeInterval(root, new Item(invoiceItem, ItemAction.ADD)));
+ break;
+
+ case REPAIR_ADJ:
+ root.addExistingItem(new NodeInterval(root, new Item(invoiceItem, ItemAction.CANCEL)));
+ break;
+
+ case FIXED:
+ existingFixedItems.add(invoiceItem);
+ break;
+
+ case ITEM_ADJ:
+ pendingItemAdj.add(invoiceItem);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Merge a new proposed ietm in the tree.
+ *
+ * @param invoiceItem new proposed item that should be merged in the existing tree
+ */
+ public void mergeProposedItem(final InvoiceItem invoiceItem) {
+
+ Preconditions.checkState(!isBuilt);
+ switch (invoiceItem.getInvoiceItemType()) {
+ case RECURRING:
+ final boolean result = root.mergeProposedItem(new NodeInterval(root, new Item(invoiceItem, ItemAction.ADD)));
+ if (!result) {
+ items.add(new Item(invoiceItem, ItemAction.ADD));
+ }
+ break;
+
+ case FIXED:
+ final InvoiceItem existingItem = Iterables.tryFind(existingFixedItems, new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ return input.matches(invoiceItem);
+ }
+ }).orNull();
+ if (existingItem == null) {
+ remainingFixedItems.add(invoiceItem);
+ }
+ break;
+
+ default:
+ Preconditions.checkState(false, "Unexpected proposed item " + invoiceItem);
+ }
+
+ }
+
+ /**
+ * Can be called prior or after merge with proposed items.
+ * <ul>
+ * <li>When called prior, the merge this gives a flat view of the existing items on disk
+ * <li>When called after the merge with proposed items, this gives the list of items that should now be written to disk -- new fixed, recurring and repair.
+ * </ul>
+ * @return a flat view of the items in the tree.
+ */
+ public List<InvoiceItem> getView() {
+
+ final List<InvoiceItem> tmp = new LinkedList<InvoiceItem>();
+ tmp.addAll(remainingFixedItems);
+ tmp.addAll(Collections2.filter(Collections2.transform(items, new Function<Item, InvoiceItem>() {
+ @Override
+ public InvoiceItem apply(final Item input) {
+ return input.toInvoiceItem();
+ }
+ }), new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(@Nullable final InvoiceItem input) {
+ return input != null;
+ }
+ }));
+
+ final List<InvoiceItem> result = Ordering.<InvoiceItem>from(INVOICE_ITEM_COMPARATOR).sortedCopy(tmp);
+ checkItemsListState(result);
+ return result;
+ }
+
+ // Verify there is no double billing, and no double repair (credits)
+ private void checkItemsListState(final List<InvoiceItem> orderedList) {
+
+ LocalDate prevRecurringEndDate = null;
+ LocalDate prevRepairEndDate = null;
+ for (InvoiceItem cur : orderedList) {
+ switch (cur.getInvoiceItemType()) {
+ case FIXED:
+ break;
+
+ case RECURRING:
+ if (prevRecurringEndDate != null) {
+ Preconditions.checkState(prevRecurringEndDate.compareTo(cur.getStartDate()) <= 0);
+ }
+ prevRecurringEndDate = cur.getEndDate();
+ break;
+
+ case REPAIR_ADJ:
+ if (prevRepairEndDate != null) {
+ Preconditions.checkState(prevRepairEndDate.compareTo(cur.getStartDate()) <= 0);
+ }
+ prevRepairEndDate = cur.getEndDate();
+ break;
+
+ default:
+ Preconditions.checkState(false, "Unexpected item type " + cur.getInvoiceItemType());
+ }
+ }
+ }
+
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SubscriptionItemTree)) {
+ return false;
+ }
+
+ final SubscriptionItemTree that = (SubscriptionItemTree) o;
+
+ if (root != null ? !root.equals(that.root) : that.root != null) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = subscriptionId != null ? subscriptionId.hashCode() : 0;
+ result = 31 * result + (root != null ? root.hashCode() : 0);
+ return result;
+ }
+
+}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
index 540c8e3..2d77b3e 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
@@ -34,6 +34,7 @@ import org.testng.annotations.Test;
import com.ning.billing.ErrorCode;
import com.ning.billing.account.api.Account;
+import com.ning.billing.callcontext.InternalCallContext;
import com.ning.billing.catalog.DefaultPrice;
import com.ning.billing.catalog.MockInternationalPrice;
import com.ning.billing.catalog.MockPlan;
@@ -45,6 +46,7 @@ import com.ning.billing.catalog.api.PhaseType;
import com.ning.billing.catalog.api.Plan;
import com.ning.billing.catalog.api.PlanPhase;
import com.ning.billing.clock.ClockMock;
+import com.ning.billing.entity.EntityPersistenceException;
import com.ning.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
import com.ning.billing.invoice.MockBillingEventSet;
import com.ning.billing.invoice.api.Invoice;
@@ -60,13 +62,11 @@ import com.ning.billing.invoice.model.DefaultInvoicePayment;
import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
import com.ning.billing.invoice.model.RecurringInvoiceItem;
import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
-import com.ning.billing.subscription.api.SubscriptionBase;
-import com.ning.billing.subscription.api.SubscriptionBaseTransitionType;
-import com.ning.billing.callcontext.InternalCallContext;
-import com.ning.billing.entity.EntityPersistenceException;
import com.ning.billing.junction.BillingEvent;
import com.ning.billing.junction.BillingEventSet;
import com.ning.billing.junction.BillingModeType;
+import com.ning.billing.subscription.api.SubscriptionBase;
+import com.ning.billing.subscription.api.SubscriptionBaseTransitionType;
import com.ning.billing.util.currency.KillBillMoney;
import com.google.common.collect.ImmutableMap;
@@ -1156,13 +1156,17 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
assertEquals(invoice.getBalance().compareTo(ZERO), 0);
}
- private SubscriptionBase getZombieSubscription() {
+ private SubscriptionBase getZombieSubscription(UUID subscriptionId) {
final SubscriptionBase subscription = Mockito.mock(SubscriptionBase.class);
Mockito.when(subscription.getId()).thenReturn(UUID.randomUUID());
Mockito.when(subscription.getBundleId()).thenReturn(UUID.randomUUID());
return subscription;
}
+ private SubscriptionBase getZombieSubscription() {
+ return getZombieSubscription(UUID.randomUUID());
+ }
+
@Test(groups = "slow")
public void testInvoiceForFreeTrialWithRecurringDiscount() throws InvoiceApiException, CatalogApiException {
final Currency currency = Currency.USD;
@@ -1282,7 +1286,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
((ClockMock) clock).setDay(startDate);
final LocalDate recuringStartDate = clock.getUTCNow().plusDays(30).toLocalDate();
- final LocalDate recuringEndDate = clock.getUTCNow().plusDays(30).toLocalDate();
+ final LocalDate recuringEndDate = recuringStartDate.plusMonths(1);
final LocalDate targetDate = recuringStartDate.plusDays(1);
// FIRST CREATE INITIAL INVOICE WITH ONE RECURRING ITEM
@@ -1317,7 +1321,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
// NOW COMPUTE A DIFFERENT ITEM TO TRIGGER REPAIR
final BillingEventSet events = new MockBillingEventSet();
- final SubscriptionBase subscription = getZombieSubscription();
+ final SubscriptionBase subscription = getZombieSubscription(subscriptionId);
final Plan plan = Mockito.mock(Plan.class);
Mockito.when(plan.getName()).thenReturn("plan");
@@ -1325,18 +1329,15 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
final PlanPhase phase1 = Mockito.mock(PlanPhase.class);
Mockito.when(phase1.getName()).thenReturn("plan-phase1");
- final PlanPhase phase2 = Mockito.mock(PlanPhase.class);
- Mockito.when(phase2.getName()).thenReturn("plan-phase2");
-
final BillingEvent event1 = invoiceUtil.createMockBillingEvent(null, subscription, recuringStartDate.toDateTimeAtStartOfDay(), plan, phase1, null,
TEN, Currency.USD,
- BillingPeriod.MONTHLY, 1, BillingModeType.IN_ADVANCE,
+ BillingPeriod.MONTHLY, 31, BillingModeType.IN_ADVANCE,
"new-event", 1L, SubscriptionBaseTransitionType.CREATE);
events.add(event1);
final Invoice newInvoice = generator.generateInvoice(UUID.randomUUID(), events, invoices, targetDate, Currency.USD);
invoiceUtil.createInvoice(newInvoice, true, context);
- // VERIFY THAT WE STILL HAVE ONLY 2 ITEMS, MENAING THERE WERE NO REPAIR AND NO CBA GENERATED
+ // VERIFY THAT WE STILL HAVE ONLY 2 ITEMS, MEANING THERE WERE NO REPAIR AND NO CBA GENERATED
final Invoice firstInvoice = new DefaultInvoice(invoiceDao.getById(invoiceId, context));
assertNotNull(firstInvoice);
assertEquals(firstInvoice.getInvoiceItems().size(), 2);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
new file mode 100644
index 0000000..f157cdf
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -0,0 +1,771 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
+import com.ning.billing.invoice.model.ItemAdjInvoiceItem;
+import com.ning.billing.invoice.model.RecurringInvoiceItem;
+import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
+
+import com.google.common.collect.Lists;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB */ {
+
+ private final UUID invoiceId = UUID.randomUUID();
+ private final UUID accountId = UUID.randomUUID();
+ private final UUID subscriptionId = UUID.randomUUID();
+ private final UUID bundleId = UUID.randomUUID();
+ private final String planName = "my-plan";
+ private final String phaseName = "my-phase";
+ private final Currency currency = Currency.USD;
+
+ @Test(groups = "fast")
+ public void testSimpleRepair() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final LocalDate repairDate = new LocalDate(2014, 1, 23);
+
+ final BigDecimal rate1 = new BigDecimal("12.00");
+ final BigDecimal amount1 = rate1;
+
+ final BigDecimal rate2 = new BigDecimal("14.85");
+ final BigDecimal amount2 = rate2;
+
+ final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+ final InvoiceItem newItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "someelse", "someelse", repairDate, endDate, amount2, rate2, currency);
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, repairDate, endDate, amount1.negate(), currency, initial.getId());
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, repairDate, new BigDecimal("8.52"), rate1, currency);
+ expectedResult.add(expected1);
+ final InvoiceItem expected2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "someelse", "someelse", repairDate, endDate, amount2, rate2, currency);
+ expectedResult.add(expected2);
+
+ // First test with items in order
+ SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(initial);
+ tree.addItem(newItem);
+ tree.addItem(repair);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+
+ tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(repair);
+ tree.addItem(newItem);
+ tree.addItem(initial);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+
+ tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(repair);
+ tree.addItem(initial);
+ tree.addItem(newItem);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMultipleRepair() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final LocalDate repairDate1 = new LocalDate(2014, 1, 23);
+
+ final LocalDate repairDate2 = new LocalDate(2014, 1, 26);
+
+ final BigDecimal rate1 = new BigDecimal("12.00");
+ final BigDecimal amount1 = rate1;
+
+ final BigDecimal rate2 = new BigDecimal("14.85");
+ final BigDecimal amount2 = rate2;
+
+ final BigDecimal rate3 = new BigDecimal("19.23");
+ final BigDecimal amount3 = rate3;
+
+ final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+ final InvoiceItem newItem1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, repairDate1, endDate, amount2, rate2, currency);
+ final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, repairDate1, endDate, amount1.negate(), currency, initial.getId());
+
+ final InvoiceItem newItem2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, repairDate2, endDate, amount3, rate3, currency);
+ final InvoiceItem repair2 = new RepairAdjInvoiceItem(invoiceId, accountId, repairDate2, endDate, amount2.negate(), currency, initial.getId());
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, repairDate1, new BigDecimal("8.52"), rate1, currency);
+ expectedResult.add(expected1);
+ final InvoiceItem expected2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, repairDate1, repairDate2, new BigDecimal("4.95"), rate2, currency);
+ expectedResult.add(expected2);
+ final InvoiceItem expected3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, repairDate2, endDate, amount3, rate3, currency);
+ expectedResult.add(expected3);
+
+ // First test with items in order
+ SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(initial);
+ tree.addItem(newItem1);
+ tree.addItem(repair1);
+ tree.addItem(newItem2);
+ tree.addItem(repair2);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+
+ tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(repair2);
+ tree.addItem(newItem1);
+ tree.addItem(newItem2);
+ tree.addItem(repair1);
+ tree.addItem(initial);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+
+ tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(repair1);
+ tree.addItem(newItem1);
+ tree.addItem(initial);
+ tree.addItem(repair2);
+ tree.addItem(newItem2);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMultipleBlockedBillings() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final LocalDate blockStart1 = new LocalDate(2014, 1, 8);
+ final LocalDate unblockStart1 = new LocalDate(2014, 1, 10);
+
+ final LocalDate blockStart2 = new LocalDate(2014, 1, 17);
+ final LocalDate unblockStart2 = new LocalDate(2014, 1, 23);
+
+ final BigDecimal rate1 = new BigDecimal("12.00");
+ final BigDecimal amount1 = rate1;
+
+ final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+ final InvoiceItem block1 = new RepairAdjInvoiceItem(invoiceId, accountId, blockStart1, unblockStart1, amount1.negate(), currency, initial.getId());
+ final InvoiceItem block2 = new RepairAdjInvoiceItem(invoiceId, accountId, blockStart2, unblockStart2, amount1.negate(), currency, initial.getId());
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, blockStart1, new BigDecimal("2.71"), rate1, currency);
+ expectedResult.add(expected1);
+ final InvoiceItem expected2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockStart1, blockStart2, new BigDecimal("2.71"), rate1, currency);
+ expectedResult.add(expected2);
+ final InvoiceItem expected3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockStart2, endDate, new BigDecimal("3.48"), rate1, currency);
+ expectedResult.add(expected3);
+
+ // First test with items in order
+ SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(initial);
+ tree.addItem(block1);
+ tree.addItem(block2);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testBlockAcrossPeriod() {
+
+ final LocalDate startDate1 = new LocalDate(2014, 1, 1);
+ final LocalDate blockDate = new LocalDate(2014, 1, 25);
+ final LocalDate startDate2 = new LocalDate(2014, 2, 1);
+ final LocalDate unblockDate = new LocalDate(2014, 2, 7);
+ final LocalDate endDate = new LocalDate(2014, 3, 1);
+
+ final BigDecimal rate1 = new BigDecimal("12.00");
+ final BigDecimal amount1 = rate1;
+
+ final InvoiceItem first = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate1, startDate2, amount1, rate1, currency);
+ final InvoiceItem second = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate2, endDate, amount1, rate1, currency);
+ final InvoiceItem block1 = new RepairAdjInvoiceItem(invoiceId, accountId, blockDate, startDate2, amount1.negate(), currency, first.getId());
+ final InvoiceItem block2 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate2, unblockDate, amount1.negate(), currency, first.getId());
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate1, blockDate, new BigDecimal("9.29"), rate1, currency);
+ final InvoiceItem expected2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockDate, endDate, new BigDecimal("9.43"), rate1, currency);
+ expectedResult.add(expected1);
+ expectedResult.add(expected2);
+
+ // First test with items in order
+ SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(first);
+ tree.addItem(second);
+ tree.addItem(block1);
+ tree.addItem(block2);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testAnnualFullRepairFollowedByMonthly() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate firstMonthlyEndDate = new LocalDate(2014, 2, 1);
+ final LocalDate secondMonthlyEndDate = new LocalDate(2014, 3, 1);
+ final LocalDate endDate = new LocalDate(2015, 2, 1);
+
+ final BigDecimal rate1 = new BigDecimal("120.00");
+ final BigDecimal amount1 = rate1;
+
+ final BigDecimal rate2 = new BigDecimal("10.00");
+ final BigDecimal amount2 = rate2;
+
+ final InvoiceItem annual = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, endDate, amount1.negate(), currency, annual.getId());
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "someelse", "someelse", startDate, firstMonthlyEndDate, amount2, rate2, currency);
+ final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "someelse", "someelse", firstMonthlyEndDate, secondMonthlyEndDate, amount2, rate2, currency);
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ expectedResult.add(monthly1);
+ expectedResult.add(monthly2);
+
+ SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(annual);
+ tree.addItem(repair);
+ tree.addItem(monthly1);
+ tree.addItem(monthly2);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+
+ tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(monthly1);
+ tree.addItem(repair);
+ tree.addItem(annual);
+ tree.addItem(monthly2);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+
+ tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(monthly1);
+ tree.addItem(monthly2);
+ tree.addItem(annual);
+ tree.addItem(repair);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMonthlyToAnnualWithLeadingProRation() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endMonthly1 = new LocalDate(2014, 2, 1);
+ final LocalDate endMonthly2 = new LocalDate(2014, 3, 1);
+ final LocalDate switchToAnnualDate = new LocalDate(2014, 2, 23);
+ final LocalDate endDate = new LocalDate(2015, 3, 1);
+
+ final BigDecimal monthlyRate = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount = monthlyRate;
+
+ final BigDecimal yearlyRate = new BigDecimal("100.00");
+ final BigDecimal yearlyAmount = yearlyRate;
+
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endMonthly1, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly1, endMonthly2, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, switchToAnnualDate, endMonthly2, monthlyAmount.negate(), currency, monthly2.getId());
+ final InvoiceItem leadingAnnualProration = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, switchToAnnualDate, endMonthly2, yearlyAmount, yearlyRate, currency);
+ final InvoiceItem annual = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly2, endDate, yearlyAmount, yearlyRate, currency);
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ expectedResult.add(monthly1);
+ final InvoiceItem monthly2Prorated = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly1, switchToAnnualDate, new BigDecimal("9.43"), monthlyRate, currency);
+ expectedResult.add(monthly2Prorated);
+ expectedResult.add(leadingAnnualProration);
+ expectedResult.add(annual);
+
+ // First test with items in order
+ SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(monthly1);
+ tree.addItem(monthly2);
+ tree.addItem(repair);
+ tree.addItem(leadingAnnualProration);
+ tree.addItem(annual);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMonthlyToAnnualWithNoProRation() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endMonthly1 = new LocalDate(2014, 2, 1);
+ final LocalDate endMonthly2 = new LocalDate(2014, 3, 1);
+ final LocalDate switchToAnnualDate = new LocalDate(2014, 2, 23);
+ final LocalDate endDate = new LocalDate(2015, 2, 23);
+
+ final BigDecimal monthlyRate = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount = monthlyRate;
+
+ final BigDecimal yearlyRate = new BigDecimal("100.00");
+ final BigDecimal yearlyAmount = yearlyRate;
+
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endMonthly1, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly1, endMonthly2, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, switchToAnnualDate, endMonthly2, monthlyAmount.negate(), currency, monthly2.getId());
+ final InvoiceItem annual = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, switchToAnnualDate, endDate, yearlyAmount, yearlyRate, currency);
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ expectedResult.add(monthly1);
+ final InvoiceItem monthly2Prorated = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly1, switchToAnnualDate, new BigDecimal("9.43"), monthlyRate, currency);
+ expectedResult.add(monthly2Prorated);
+ expectedResult.add(annual);
+
+ // First test with items in order
+ SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(monthly1);
+ tree.addItem(monthly2);
+ tree.addItem(repair);
+ tree.addItem(annual);
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMergeWithNoExisting() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final BigDecimal monthlyRate = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount = monthlyRate;
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+ tree.mergeProposedItem(proposed1);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ expectedResult.add(proposed1);
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMergeTwoSimilarItems() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final BigDecimal monthlyRate = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount = monthlyRate;
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+ tree.addItem(monthly1);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+
+ tree.mergeProposedItem(proposed1);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMergeTwoDifferentItems() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final BigDecimal monthlyRate1 = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount1 = monthlyRate1;
+
+ final BigDecimal monthlyRate2 = new BigDecimal("15.00");
+ final BigDecimal monthlyAmount2 = monthlyRate2;
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount1, monthlyRate1, currency);
+ tree.addItem(monthly1);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount2, monthlyRate2, currency);
+
+ tree.mergeProposedItem(proposed1);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, endDate, monthlyAmount1.negate(), currency, monthly1.getId());
+ expectedResult.add(proposed1);
+ expectedResult.add(repair);
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMergeCancellationWithInitialRepair() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate blockDate = new LocalDate(2014, 1, 25);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final BigDecimal monthlyRate1 = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount1 = monthlyRate1;
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount1, monthlyRate1, currency);
+ tree.addItem(monthly1);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, blockDate, endDate, monthlyAmount1, monthlyRate1, currency);
+
+ tree.mergeProposedItem(proposed1);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, blockDate, new BigDecimal("-9.29"), currency, monthly1.getId());
+ expectedResult.add(repair);
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMergeCancellationWithFinalRepair() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate cancelDate = new LocalDate(2014, 1, 25);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final BigDecimal monthlyRate1 = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount1 = monthlyRate1;
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount1, monthlyRate1, currency);
+ tree.addItem(monthly1);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, cancelDate, monthlyAmount1, monthlyRate1, currency);
+ tree.mergeProposedItem(proposed1);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, cancelDate, endDate, new BigDecimal("-2.71"), currency, monthly1.getId());
+ expectedResult.add(repair);
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMergeCancellationWithMiddleRepair() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate blockDate = new LocalDate(2014, 1, 13);
+ final LocalDate unblockDate = new LocalDate(2014, 1, 25);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final BigDecimal monthlyRate1 = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount1 = monthlyRate1;
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount1, monthlyRate1, currency);
+ tree.addItem(monthly1);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, blockDate, monthlyAmount1, monthlyRate1, currency);
+ final InvoiceItem proposed2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockDate, endDate, monthlyAmount1, monthlyRate1, currency);
+
+ tree.mergeProposedItem(proposed1);
+ tree.mergeProposedItem(proposed2);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, blockDate, unblockDate, new BigDecimal("-4.65"), currency, monthly1.getId());
+ expectedResult.add(repair);
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMergeCancellationWithTwoMiddleRepair() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate blockDate1 = new LocalDate(2014, 1, 7);
+ final LocalDate unblockDate1 = new LocalDate(2014, 1, 13);
+ final LocalDate blockDate2 = new LocalDate(2014, 1, 17);
+ final LocalDate unblockDate2 = new LocalDate(2014, 1, 25);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final BigDecimal monthlyRate = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount = monthlyRate;
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ final InvoiceItem monthly = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+ tree.addItem(monthly);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, blockDate1, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem proposed2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockDate1, blockDate2, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem proposed3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockDate2, endDate, monthlyAmount, monthlyRate, currency);
+
+ tree.mergeProposedItem(proposed1);
+ tree.mergeProposedItem(proposed2);
+ tree.mergeProposedItem(proposed3);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, blockDate1, unblockDate1, new BigDecimal("-2.32"), currency, monthly.getId());
+ final InvoiceItem repair2 = new RepairAdjInvoiceItem(invoiceId, accountId, blockDate2, unblockDate2, new BigDecimal("-3.10"), currency, monthly.getId());
+ expectedResult.add(repair1);
+ expectedResult.add(repair2);
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMergeUpgradeWithFinalRepair() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate upgradeDate = new LocalDate(2014, 1, 25);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final BigDecimal monthlyRate1 = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount1 = monthlyRate1;
+
+ final BigDecimal monthlyRate2 = new BigDecimal("20.00");
+ final BigDecimal monthlyAmount2 = monthlyRate1;
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount1, monthlyRate1, currency);
+ tree.addItem(monthly1);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, upgradeDate, monthlyAmount1, monthlyRate1, currency);
+ final InvoiceItem proposed2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "foo", "foo", upgradeDate, endDate, monthlyAmount2, monthlyRate2, currency);
+ tree.mergeProposedItem(proposed1);
+ tree.mergeProposedItem(proposed2);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, upgradeDate, endDate, new BigDecimal("-2.71"), currency, monthly1.getId());
+ expectedResult.add(proposed2);
+ expectedResult.add(repair);
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMergeWithSecondRepair() {
+
+ final LocalDate startDate = new LocalDate(2012, 5, 1);
+ final LocalDate endDate = new LocalDate(2012, 6, 1);
+ final LocalDate change1 = new LocalDate(2012, 5, 7);
+ final LocalDate change2 = new LocalDate(2012, 5, 8);
+
+ final BigDecimal rate1 = new BigDecimal("599.95");
+ final BigDecimal amount1 = rate1;
+
+ final BigDecimal rate2 = new BigDecimal("9.95");
+ final BigDecimal proratedAmount2 = new BigDecimal("8.02");
+
+ final BigDecimal rate3 = new BigDecimal("29.95");
+ final BigDecimal proratedAmount3 = new BigDecimal("23.19");
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+ final InvoiceItem newItem1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "foo", "foo", change1, endDate, proratedAmount2, rate2, currency);
+ final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, change1, endDate, new BigDecimal("-483.86"), currency, initial.getId());
+
+ tree.addItem(initial);
+ tree.addItem(newItem1);
+ tree.addItem(repair1);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, change1, amount1, rate1, currency);
+ final InvoiceItem proposed2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "foo", "foo", change1, change2, proratedAmount3, rate2, currency);
+ final InvoiceItem proposed3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "bar", "bar", change2, endDate, proratedAmount3, rate3, currency);
+ tree.mergeProposedItem(proposed1);
+ tree.mergeProposedItem(proposed2);
+ tree.mergeProposedItem(proposed3);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem repair2 = new RepairAdjInvoiceItem(invoiceId, accountId, change2, endDate, new BigDecimal("-7.70"), currency, initial.getId());
+ expectedResult.add(proposed3);
+ expectedResult.add(repair2);
+ verifyResult(tree.getView(), expectedResult);
+
+ }
+
+ @Test(groups = "fast")
+ public void testWithExistingFixedItem() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final BigDecimal monthlyRate = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount = monthlyRate;
+ final BigDecimal fixedAmount = new BigDecimal("5.00");
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ final InvoiceItem monthly = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem fixed = new FixedPriceInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, fixedAmount, currency);
+ tree.addItem(monthly);
+ tree.addItem(fixed);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+ tree.mergeProposedItem(proposed1);
+ tree.mergeProposedItem(fixed);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testWithNewFixedItem() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final BigDecimal monthlyRate = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount = monthlyRate;
+ final BigDecimal fixedAmount = new BigDecimal("5.00");
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ final InvoiceItem monthly = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+ tree.addItem(monthly);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem fixed = new FixedPriceInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, fixedAmount, currency);
+ tree.mergeProposedItem(proposed1);
+ tree.mergeProposedItem(fixed);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ expectedResult.add(fixed);
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testRepairWithSmallItemAdjustment() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate itemAdjDate = new LocalDate(2014, 1, 2);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final LocalDate cancelDate = new LocalDate(2014, 1, 23);
+
+ final BigDecimal rate1 = new BigDecimal("12.00");
+ final BigDecimal amount1 = rate1;
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+ final InvoiceItem itemAdj = new ItemAdjInvoiceItem(initial, itemAdjDate, new BigDecimal("-2.00"), currency);
+ tree.addItem(initial);
+ tree.addItem(itemAdj);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, cancelDate, amount1, rate1, currency);
+ tree.mergeProposedItem(proposed1);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, cancelDate, endDate, new BigDecimal("-3.48"), currency, initial.getId());
+ expectedResult.add(repair1);
+
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testRepairWithLargeItemAdjustment() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate itemAdjDate = new LocalDate(2014, 1, 2);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final LocalDate cancelDate = new LocalDate(2014, 1, 23);
+
+ final BigDecimal rate1 = new BigDecimal("12.00");
+ final BigDecimal amount1 = rate1;
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+ final InvoiceItem itemAdj = new ItemAdjInvoiceItem(initial, itemAdjDate, new BigDecimal("-10.00"), currency);
+ tree.addItem(initial);
+ tree.addItem(itemAdj);
+ tree.flatten(true);
+
+ final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, cancelDate, amount1, rate1, currency);
+ tree.mergeProposedItem(proposed1);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, cancelDate, endDate, new BigDecimal("-2.00"), currency, initial.getId());
+ expectedResult.add(repair1);
+
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMergeMonthlyToAnnualWithNoProRation() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endMonthly1 = new LocalDate(2014, 2, 1);
+ final LocalDate endMonthly2 = new LocalDate(2014, 3, 1);
+ final LocalDate switchToAnnualDate = new LocalDate(2014, 2, 23);
+ final LocalDate endDate = new LocalDate(2015, 2, 23);
+
+ final BigDecimal monthlyRate = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount = monthlyRate;
+
+ final BigDecimal yearlyRate = new BigDecimal("100.00");
+ final BigDecimal yearlyAmount = yearlyRate;
+
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endMonthly1, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly1, endMonthly2, monthlyAmount, monthlyRate, currency);
+
+ // First test with items in order
+ SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(monthly1);
+ tree.addItem(monthly2);
+ tree.flatten(true);
+
+ final InvoiceItem proposed = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, switchToAnnualDate, endDate, yearlyAmount, yearlyRate, currency);
+ final InvoiceItem proposedMonthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endMonthly1, monthlyAmount, monthlyRate, currency);
+ tree.mergeProposedItem(proposedMonthly1);
+ final InvoiceItem proRatedmonthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly1, switchToAnnualDate, monthlyAmount, monthlyRate, currency);
+ tree.mergeProposedItem(proRatedmonthly2);
+ tree.mergeProposedItem(proposed);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, switchToAnnualDate, endMonthly2, new BigDecimal("-2.57"), currency, monthly2.getId());
+ expectedResult.add(proposed);
+ expectedResult.add(repair);
+
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ private void verifyResult(final List<InvoiceItem> result, final List<InvoiceItem> expectedResult) {
+ assertEquals(result.size(), expectedResult.size());
+ for (int i = 0; i < expectedResult.size(); i++) {
+ assertTrue(result.get(i).matches(expectedResult.get(i)));
+ }
+ }
+
+}