Skip to content

Commit

Permalink
beatrix: Add test scenario to show how to void invoices with bad data…
Browse files Browse the repository at this point in the history
… and exit __PARK__ state.
  • Loading branch information
sbrossie committed Sep 12, 2019
1 parent b809702 commit e165eba
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 1 deletion.
Expand Up @@ -20,10 +20,13 @@
import java.math.BigDecimal;
import java.util.UUID;

import javax.inject.Inject;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
Expand All @@ -32,20 +35,35 @@
import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.DefaultEntitlementSpecifier;
import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoiceStatus;
import org.killbill.billing.invoice.dao.InvoiceDao;
import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
import org.killbill.billing.invoice.dao.InvoiceModelDao;
import org.killbill.billing.invoice.dao.InvoiceTrackingModelDao;
import org.killbill.billing.junction.DefaultBlockingState;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.util.tag.ControlTagType;
import org.testng.Assert;
import org.testng.annotations.Test;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

import static org.killbill.billing.ErrorCode.INVOICE_NOTHING_TO_DO;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.fail;

public class TestWithInvoiceHardening extends TestIntegrationBase {


@Inject
protected InvoiceDao invoiceDao;


@Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/1205")
public void testFor1205() throws Exception {

Expand Down Expand Up @@ -200,6 +218,128 @@ public void testBasicBlockUnblock() throws Exception {

}



@Test(groups = "slow")
public void testFixParkedAccountByVoidingInvoices() throws Exception {

final DateTimeZone testTimeZone = DateTimeZone.UTC;

final DateTime initialDate = new DateTime(2019, 4, 27, 0, 13, 42, 0, testTimeZone);
clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());

final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
assertNotNull(account);

add_AUTO_PAY_OFF_Tag(account.getId(), ObjectType.ACCOUNT);

add_AUTO_INVOICING_OFF_Tag(account.getId(), ObjectType.ACCOUNT);

busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK);
final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "notrial", null);
UUID entitlementId = entitlementApi.createBaseEntitlement(account.getId(), new DefaultEntitlementSpecifier(spec), "Something", null, null, false, true, ImmutableList.<PluginProperty>of(), callContext);
assertListenerStatus();


final BlockingState blockingState2 = new DefaultBlockingState(entitlementId, BlockingStateType.SUBSCRIPTION, "SOMETHING_BLOCK", "company.a.b.c", true, true, true, null);
subscriptionApi.addBlockingState(blockingState2, new LocalDate(2019, 5, 17), ImmutableList.<PluginProperty>of(), callContext);

// 2019-05-17
busHandler.pushExpectedEvents(NextEvent.BLOCK);
clock.addDays(20);
assertListenerStatus();

//
// Simulate some bad data on disk
//

// Invoice 1: RECURRING 2019-4-27 -> 2019-5-27
final InvoiceModelDao firstInvoice = new InvoiceModelDao(UUID.randomUUID(), clock.getUTCNow(), account.getId(), null, new LocalDate(2019, 4, 27), new LocalDate(2019, 4, 27), account.getCurrency(), false, InvoiceStatus.COMMITTED, false);
final UUID initialRecuringItemId = UUID.randomUUID();
firstInvoice.addInvoiceItem(new InvoiceItemModelDao(initialRecuringItemId, clock.getUTCNow(), InvoiceItemType.RECURRING, firstInvoice.getId(), account.getId(), null, null, entitlementId, "",
"Blowdart", "blowdart-monthly-notrial", "blowdart-monthly-notrial-evergreen", null,
new LocalDate(2019, 4, 27), new LocalDate(2019, 5, 27),
new BigDecimal("29.95"), new BigDecimal("29.95"), account.getCurrency(), null, null, null));

insertInvoiceItems(firstInvoice);

// Invoice 2: REPAIR_ADJ 2019-5-3 -> 2019-5-27 => Simulate the BLOCK billing on 2019-5-3
final InvoiceModelDao secondInvoice = new InvoiceModelDao(UUID.randomUUID(), clock.getUTCNow(), account.getId(), null, new LocalDate(2019, 5, 3), new LocalDate(2019, 5, 3), account.getCurrency(), false, InvoiceStatus.COMMITTED, false);
secondInvoice.addInvoiceItem(new InvoiceItemModelDao(UUID.randomUUID(), clock.getUTCNow(), InvoiceItemType.REPAIR_ADJ, secondInvoice.getId(), account.getId(), null, null, entitlementId, "",
null, null, null, null,
new LocalDate(2019, 5, 3), new LocalDate(2019, 5, 27),
new BigDecimal("-23.96"), null, account.getCurrency(), initialRecuringItemId, null, null));
insertInvoiceItems(secondInvoice);

// Invoice 3: REPAIR_ADJ 2019-4-27 -> 2019-5-03 => Simulate the UNBLOCK billing on 2019-5-3 and RECURRING 2019-4-27 -> 2019-5-27
// Original initialRecuringItemId is now fully repaired and we have re-invoiced for the full period
//
final InvoiceModelDao thirdInvoice = new InvoiceModelDao(UUID.randomUUID(), clock.getUTCNow(), account.getId(), null, new LocalDate(2019, 5, 3), new LocalDate(2019, 5, 3), account.getCurrency(), false, InvoiceStatus.COMMITTED, false);
final UUID secondRecurringItemId = UUID.randomUUID();
thirdInvoice.addInvoiceItem(new InvoiceItemModelDao(secondRecurringItemId, clock.getUTCNow(), InvoiceItemType.RECURRING, thirdInvoice.getId(), account.getId(), null, null, entitlementId, "",
"Blowdart", "blowdart-monthly-notrial", "blowdart-monthly-notrial-evergreen", null,
new LocalDate(2019, 4, 27), new LocalDate(2019, 5, 27),
new BigDecimal("29.95"), new BigDecimal("29.95"), account.getCurrency(), null, null, null));

thirdInvoice.addInvoiceItem(new InvoiceItemModelDao(UUID.randomUUID(), clock.getUTCNow(), InvoiceItemType.REPAIR_ADJ, thirdInvoice.getId(), account.getId(), null, null, entitlementId, "",
null, null, null, null,
new LocalDate(2019, 4, 27), new LocalDate(2019, 5, 03),
new BigDecimal("-5.99"), null, account.getCurrency(), initialRecuringItemId, null, null));
insertInvoiceItems(thirdInvoice);

// Invoice 4: REPAIR_ADJ 2019-5-17 -> 2019-5-27 => Simulate the BLOCK billing on 2019-5-17
final InvoiceModelDao fourthInvoice = new InvoiceModelDao(UUID.randomUUID(), clock.getUTCNow(), account.getId(), null, new LocalDate(2019, 5, 17), new LocalDate(2019, 5, 17), account.getCurrency(), false, InvoiceStatus.COMMITTED, false);

fourthInvoice.addInvoiceItem(new InvoiceItemModelDao(UUID.randomUUID(), clock.getUTCNow(), InvoiceItemType.REPAIR_ADJ, fourthInvoice.getId(), account.getId(), null, null, entitlementId, "",
null, null, null, null,
new LocalDate(2019, 5, 17), new LocalDate(2019, 5, 27),
new BigDecimal("-9.98"), null, account.getCurrency(), secondRecurringItemId, null, null));

insertInvoiceItems(fourthInvoice);

// Invoice 5: RECURRING 2019-4-17 -> 2019-5-17
final InvoiceModelDao fifthInvoice = new InvoiceModelDao(UUID.randomUUID(), clock.getUTCNow(), account.getId(), null, new LocalDate(2019, 5, 17), new LocalDate(2019, 5, 17), account.getCurrency(), false, InvoiceStatus.COMMITTED, false);
final UUID thirdRecurringItemId = UUID.randomUUID();
fifthInvoice.addInvoiceItem(new InvoiceItemModelDao(thirdRecurringItemId, clock.getUTCNow(), InvoiceItemType.RECURRING, fifthInvoice.getId(), account.getId(), null, null, entitlementId, "",
"Blowdart", "blowdart-monthly-notrial", "blowdart-monthly-notrial-evergreen", null,
new LocalDate(2019, 4, 27), new LocalDate(2019, 5, 17),
new BigDecimal("5.99"), new BigDecimal("29.95"), account.getCurrency(), null, null, null));

insertInvoiceItems(fifthInvoice);


// Trigger IllegalStateException: Double billing detected
busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE, NextEvent.INVOICE, NextEvent.INVOICE, NextEvent.INVOICE, NextEvent.TAG);
tagUserApi.removeTag(account.getId(), ObjectType.ACCOUNT, ControlTagType.AUTO_INVOICING_OFF.getId(), callContext);
busHandler.waitAndIgnoreEvents(3000);

invoiceUserApi.voidInvoice(firstInvoice.getId(), callContext);
invoiceUserApi.voidInvoice(secondInvoice.getId(), callContext);

invoiceUserApi.voidInvoice(thirdInvoice.getId(), callContext);
invoiceUserApi.voidInvoice(fourthInvoice.getId(), callContext);

// This remove the __PARK__ tag and fixes the state !
busHandler.pushExpectedEvents(NextEvent.TAG, NextEvent.NULL_INVOICE);
try {
invoiceUserApi.triggerInvoiceGeneration(account.getId(), new LocalDate(2019, 5, 17), callContext);
fail();
} catch (final InvoiceApiException e) {
assertEquals(e.getCode(), INVOICE_NOTHING_TO_DO.getCode());
} finally {
assertListenerStatus();
}

}



private void insertInvoiceItems(final InvoiceModelDao invoice) {

final FutureAccountNotifications callbackDateTimePerSubscriptions = new FutureAccountNotifications();

invoiceDao.createInvoice(invoice, ImmutableSet.<InvoiceTrackingModelDao>of(), callbackDateTimePerSubscriptions, internalCallContext);
}
private void verifyNoInvoiceDueOnDate(final UUID accountId, final LocalDate targetDate) {
busHandler.pushExpectedEvents(NextEvent.NULL_INVOICE);
try {
Expand Down
17 changes: 16 additions & 1 deletion util/src/test/java/org/killbill/billing/api/TestApiListener.java
Expand Up @@ -63,7 +63,7 @@ public class TestApiListener {

private static final Joiner SPACE_JOINER = Joiner.on(" ");

private static final long DELAY = 60000;
private static final long DELAY = 10000;

private final List<NextEvent> nextExpectedEvent;
private final IDBI idbi;
Expand All @@ -82,6 +82,21 @@ public TestApiListener(final IDBI idbi, final Clock clock) {
this.clock = clock;
}

// In some (rare) cases we don't want to listen to events -- e.g event processing causes exceptions
public void waitAndIgnoreEvents(long waitMsec) {

long remaining = waitMsec;
try {
do {
Thread.sleep(100);
remaining -= 100;
} while(remaining > 0);
} catch(InterruptedException ignored) {
} finally {
reset();
}
}

public void assertListenerStatus() {
// Bail early
if (isListenerFailed) {
Expand Down

0 comments on commit e165eba

Please sign in to comment.