Skip to content

Commit

Permalink
#459 Invoices changes to support Hierarchical Account
Browse files Browse the repository at this point in the history
  • Loading branch information
matias-aguero-hs committed Feb 4, 2016
1 parent 74519e7 commit c3c7cc9
Show file tree
Hide file tree
Showing 36 changed files with 701 additions and 55 deletions.
Expand Up @@ -28,16 +28,21 @@ public class DefaultImmutableAccountData implements ImmutableAccountData {
private final String externalKey;
private final Currency currency;
private final DateTimeZone dateTimeZone;
private final UUID parentAccountId;
private final boolean isPaymentDelegatedToParent;

public DefaultImmutableAccountData(final UUID id, final String externalKey, final Currency currency, final DateTimeZone dateTimeZone) {
public DefaultImmutableAccountData(final UUID id, final String externalKey, final Currency currency, final DateTimeZone dateTimeZone,
final UUID parentAccountId, final boolean isPaymentDelegatedToParent) {
this.id = id;
this.externalKey = externalKey;
this.currency = currency;
this.dateTimeZone = dateTimeZone;
this.parentAccountId = parentAccountId;
this.isPaymentDelegatedToParent = isPaymentDelegatedToParent;
}

public DefaultImmutableAccountData(final Account account) {
this(account.getId(), account.getExternalKey(), account.getCurrency(), account.getTimeZone());
this(account.getId(), account.getExternalKey(), account.getCurrency(), account.getTimeZone(), account.getParentAccountId(), account.isPaymentDelegatedToParent());
}

@Override
Expand All @@ -59,4 +64,15 @@ public Currency getCurrency() {
public DateTimeZone getTimeZone() {
return dateTimeZone;
}

@Override
public UUID getParentAccountId() {
return parentAccountId;
}

@Override
public Boolean isPaymentDelegatedToParent() {
return isPaymentDelegatedToParent;
}

}
Expand Up @@ -372,6 +372,23 @@ protected AccountData getAccountData(final int billingDay) {
.build();
}

protected AccountData getChildAccountData(final int billingDay, final UUID parentAccountId, final boolean isPaymentDelegatedToParent) {
return new MockAccountBuilder().name(UUID.randomUUID().toString().substring(1, 8))
.firstNameLength(6)
.email(UUID.randomUUID().toString().substring(1, 8))
.phone(UUID.randomUUID().toString().substring(1, 8))
.migrated(false)
.isNotifiedForInvoices(false)
.externalKey(UUID.randomUUID().toString().substring(1, 8))
.billingCycleDayLocal(billingDay)
.currency(Currency.USD)
.paymentMethodId(UUID.randomUUID())
.timeZone(DateTimeZone.UTC)
.parentAccountId(parentAccountId)
.isPaymentDelegatedToParent(isPaymentDelegatedToParent)
.build();
}

protected void addMonthsAndCheckForCompletion(final int nbMonth, final NextEvent... events) {
doCallAndCheckForCompletion(new Function<Void, Void>() {
@Override
Expand Down
Expand Up @@ -27,6 +27,7 @@
import org.joda.time.LocalDate;
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountData;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
import org.killbill.billing.catalog.api.BillingPeriod;
Expand All @@ -37,6 +38,7 @@
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoiceStatus;
import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PluginProperty;
Expand Down Expand Up @@ -335,4 +337,71 @@ public void testDraftInvoice() throws Exception {

}

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

final int billingDay = 14;
final DateTime initialCreationDate = new DateTime(2015, 5, 15, 0, 0, 0, 0, testTimeZone);

log.info("Beginning test with BCD of " + billingDay);
final Account parentAccount = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
final Account child1Account = createAccountWithNonOsgiPaymentMethod(getChildAccountData(billingDay, parentAccount.getId(), true));
final Account child2Account = createAccountWithNonOsgiPaymentMethod(getChildAccountData(billingDay, parentAccount.getId(), true));

// set clock to the initial start date
clock.setTime(initialCreationDate);

//
// CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
//
DefaultEntitlement baseEntitlementChild1 = createBaseEntitlementAndCheckForCompletion(child1Account.getId(), "bundleKey1", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
DefaultEntitlement baseEntitlementChild2 = createBaseEntitlementAndCheckForCompletion(child2Account.getId(), "bundleKey2", "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);

// First Parent invoice over TRIAL period

List<Invoice> parentInvoices = invoiceUserApi.getInvoicesByAccount(parentAccount.getId(), callContext);
assertEquals(parentInvoices.size(), 1);

Invoice parentInvoice = parentInvoices.get(0);
assertEquals(parentInvoice.getNumberOfItems(), 2);
assertEquals(parentInvoice.getStatus(), InvoiceStatus.DRAFT);
assertTrue(parentInvoice.isParentInvoice());
//assertEquals(parentInvoice.getBalance(), BigDecimal.ZERO);

// No payment is expected because balance is 0
busHandler.pushExpectedEvents(NextEvent.INVOICE);
invoiceUserApi.commitInvoice(parentInvoice.getId(), callContext);
assertListenerStatus();

parentInvoice = invoiceUserApi.getInvoice(parentInvoice.getId(), callContext);
assertEquals(parentInvoice.getStatus(), InvoiceStatus.COMMITTED);

// Move through time and verify new parent Invoice
busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.PHASE,
NextEvent.INVOICE, NextEvent.INVOICE,
NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT,
NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT); // TODO fix when refactor invoice.getBalance
clock.addDays(31);
assertListenerStatus();

// Second Parent invoice over Recurring period

parentInvoices = invoiceUserApi.getInvoicesByAccount(parentAccount.getId(), callContext);
assertEquals(parentInvoices.size(), 2);

parentInvoice = parentInvoices.get(1);
assertEquals(parentInvoice.getNumberOfItems(), 2);
assertEquals(parentInvoice.getStatus(), InvoiceStatus.DRAFT);
assertTrue(parentInvoice.isParentInvoice());

// now payment is expected
busHandler.pushExpectedEvents(NextEvent.INVOICE); // TODO NextEvent.PAYMENT
invoiceUserApi.commitInvoice(parentInvoice.getId(), callContext);
assertListenerStatus();

parentInvoice = invoiceUserApi.getInvoice(parentInvoice.getId(), callContext);
assertEquals(parentInvoice.getStatus(), InvoiceStatus.COMMITTED);

}

}
Expand Up @@ -34,6 +34,7 @@
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
Expand Down Expand Up @@ -65,16 +66,19 @@
import org.killbill.billing.invoice.api.user.DefaultInvoiceCreationEvent;
import org.killbill.billing.invoice.api.user.DefaultInvoiceNotificationInternalEvent;
import org.killbill.billing.invoice.api.user.DefaultNullInvoiceEvent;
import org.killbill.billing.invoice.calculator.InvoiceCalculatorUtils;
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.InvoiceParentChildModelDao;
import org.killbill.billing.invoice.generator.InvoiceGenerator;
import org.killbill.billing.invoice.generator.InvoiceWithMetadata;
import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates.UsageDef;
import org.killbill.billing.invoice.model.DefaultInvoice;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
import org.killbill.billing.invoice.model.InvoiceItemFactory;
import org.killbill.billing.invoice.model.ParentInvoiceItem;
import org.killbill.billing.invoice.model.RecurringInvoiceItem;
import org.killbill.billing.invoice.notification.DefaultNextBillingDateNotifier;
import org.killbill.billing.invoice.notification.NextBillingDateNotificationKey;
Expand Down Expand Up @@ -700,4 +704,35 @@ public List<PlanPhasePriceOverride> getPlanPhasePriceOverrides() {
return null;
}
}

public void processParentInvoiceForInvoiceGeneration(final ImmutableAccountData account, final UUID invoiceId, final InternalCallContext context) throws InvoiceApiException {

final InvoiceModelDao invoiceModelDao = invoiceDao.getById(invoiceId, context);
final Invoice invoice = new DefaultInvoice(invoiceModelDao);

BigDecimal invoiceAmount = InvoiceCalculatorUtils.computeInvoiceBalance(invoice.getCurrency(), invoice.getInvoiceItems(), invoice.getPayments());
InvoiceModelDao parentInvoice = invoiceDao.getParentDraftInvoice(account.getParentAccountId(), context);

final Long parentAccountRecordId = internalCallContextFactory.getRecordIdFromObject(account.getParentAccountId(), ObjectType.ACCOUNT, buildTenantContext(context));
final InternalCallContext parentContext = new InternalCallContext(context, parentAccountRecordId);

if (parentInvoice != null) {
InvoiceItem invoiceItem = new ParentInvoiceItem(UUID.randomUUID(), DateTime.now(), parentInvoice.getId(), account.getParentAccountId(), account.getId(), invoiceAmount, account.getCurrency());
parentInvoice.addInvoiceItem(new InvoiceItemModelDao(invoiceItem));
List<InvoiceModelDao> invoices = new ArrayList<InvoiceModelDao>();
invoices.add(parentInvoice);
invoiceDao.createInvoices(invoices, parentContext);
} else {
parentInvoice = new InvoiceModelDao(account.getParentAccountId(), LocalDate.now(), account.getCurrency(), InvoiceStatus.DRAFT, true);
InvoiceItem invoiceItem = new ParentInvoiceItem(UUID.randomUUID(), DateTime.now(), parentInvoice.getId(), account.getParentAccountId(), account.getId(), invoiceAmount, account.getCurrency());
parentInvoice.addInvoiceItem(new InvoiceItemModelDao(invoiceItem));
invoiceDao.createInvoice(parentInvoice, parentInvoice.getInvoiceItems(), true, null, parentContext);
}

// save parent child invoice relation
final InvoiceParentChildModelDao invoiceRelation = new InvoiceParentChildModelDao(parentInvoice.getId(), invoiceId, account.getId());
invoiceDao.createParentChildInvoiceRelation(invoiceRelation, parentContext);

}

}
Expand Up @@ -19,23 +19,26 @@
import java.util.UUID;

import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.clock.Clock;
import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.events.BlockingTransitionInternalEvent;
import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.events.EffectiveEntitlementInternalEvent;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
import org.killbill.billing.events.InvoiceCreationInternalEvent;
import org.killbill.billing.events.RepairSubscriptionInternalEvent;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceInternalApi;
import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.util.callcontext.CallOrigin;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.UserType;
import org.killbill.billing.events.EffectiveEntitlementInternalEvent;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
import org.killbill.billing.events.RepairSubscriptionInternalEvent;
import org.killbill.billing.util.config.InvoiceConfig;
import org.killbill.clock.Clock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.eventbus.AllowConcurrentEvents;
import com.google.common.eventbus.Subscribe;
Expand All @@ -48,17 +51,19 @@ public class InvoiceListener {
private final InvoiceDispatcher dispatcher;
private final InternalCallContextFactory internalCallContextFactory;
private final AccountInternalApi accountApi;
private final InvoiceInternalApi invoiceApi;
private final InvoiceConfig invoiceConfig;
private final Clock clock;

@Inject
public InvoiceListener(final AccountInternalApi accountApi, final Clock clock, final InternalCallContextFactory internalCallContextFactory,
final InvoiceConfig invoiceConfig, final InvoiceDispatcher dispatcher) {
final InvoiceConfig invoiceConfig, final InvoiceDispatcher dispatcher, InvoiceInternalApi invoiceApi) {
this.accountApi = accountApi;
this.dispatcher = dispatcher;
this.invoiceConfig = invoiceConfig;
this.internalCallContextFactory = internalCallContextFactory;
this.clock = clock;
this.invoiceApi = invoiceApi;
}

@AllowConcurrentEvents
Expand Down Expand Up @@ -141,4 +146,29 @@ public void handleEventForInvoiceNotification(final UUID subscriptionId, final D
log.error(e.getMessage());
}
}

@AllowConcurrentEvents
@Subscribe
public void handleChildrenInvoiceCreationEvent(final InvoiceCreationInternalEvent event) {

try {
final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "CreateParentInvoice", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
final ImmutableAccountData account = accountApi.getImmutableAccountDataById(event.getAccountId(), context);

// catch children invoices and populate the parent summary invoice
if (isChildrenAccountAndPaymentDelegated(account)) {
dispatcher.processParentInvoiceForInvoiceGeneration(account, event.getInvoiceId(), context);
}

} catch (InvoiceApiException e) {
log.error(e.getMessage());
} catch (AccountApiException e) {
log.error(e.getMessage());
}
}

private boolean isChildrenAccountAndPaymentDelegated(final ImmutableAccountData account) {
return account.getParentAccountId() != null && account.isPaymentDelegatedToParent();
}

}
Expand Up @@ -85,7 +85,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
.onResultOf(new Function<InvoiceModelDao, Comparable>() {
@Override
public Comparable apply(final InvoiceModelDao invoice) {
return invoice.getTargetDate();
return invoice.getTargetDate() == null ? invoice.getCreatedDate() : invoice.getTargetDate();
}
});

Expand Down Expand Up @@ -268,7 +268,9 @@ public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFa
createInvoiceItemFromTransaction(transInvoiceItemSqlDao, invoiceItemModelDao, context);
}
cbaDao.addCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
notifyOfFutureBillingEvents(entitySqlDaoWrapperFactory, invoice.getAccountId(), callbackDateTimePerSubscriptions, context);
if (InvoiceStatus.COMMITTED.equals(invoice.getStatus())) {
notifyOfFutureBillingEvents(entitySqlDaoWrapperFactory, invoice.getAccountId(), callbackDateTimePerSubscriptions, context);
}
}
return null;
}
Expand Down Expand Up @@ -946,6 +948,7 @@ public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFa
if (InvoiceStatus.COMMITTED.equals(newStatus)) {
// notify invoice creation event
notifyBusOfInvoiceCreation(entitySqlDaoWrapperFactory, invoice, context);
// notifyOfFutureBillingEvents(entitySqlDaoWrapperFactory, invoice.getAccountId(), callbackDateTimePerSubscriptions, context);
}

return null;
Expand All @@ -966,4 +969,26 @@ private void notifyBusOfInvoiceCreation(final EntitySqlDaoWrapperFactory entityS
}
}

@Override
public void createParentChildInvoiceRelation(final InvoiceParentChildModelDao invoiceRelation, final InternalCallContext context) throws InvoiceApiException {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceParentChildrenSqlDao transactional = entitySqlDaoWrapperFactory.become(InvoiceParentChildrenSqlDao.class);
transactional.create(invoiceRelation, context);
return null;
}
});
}

@Override
public InvoiceModelDao getParentDraftInvoice(final UUID parentAccountId, final InternalCallContext context) throws InvoiceApiException {
return transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<InvoiceModelDao>() {
@Override
public InvoiceModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoiceSqlDao invoiceSqlDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
return invoiceSqlDao.getParentDraftInvoice(parentAccountId.toString(), context);
}
});
}
}

0 comments on commit c3c7cc9

Please sign in to comment.