killbill-aplcache
invoice: Refactor DefaultInvoiceGenerator and remove BillingModeGenerator Pure …
Changes
invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java 350(+8 -342)
invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringGenerator.java 18(+5 -13)
invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java 338(+338 -0)
invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java 193(+193 -0)
invoice/src/main/java/org/killbill/billing/invoice/model/DefaultBillingModeGenerator.java 131(+0 -131)
invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java 1(+0 -1)
Details
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
index 2d6e805..6447349 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
@@ -18,11 +18,7 @@
package org.killbill.billing.invoice.generator;
-import java.math.BigDecimal;
-import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -34,61 +30,33 @@ import org.joda.time.Months;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.callcontext.InternalCallContext;
-import org.killbill.billing.catalog.api.BillingMode;
-import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Currency;
-import org.killbill.billing.catalog.api.Usage;
-import org.killbill.billing.catalog.api.UsageType;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
-import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
-import org.killbill.billing.invoice.model.BillingModeGenerator;
-import org.killbill.billing.invoice.model.DefaultBillingModeGenerator;
import org.killbill.billing.invoice.model.DefaultInvoice;
-import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
-import org.killbill.billing.invoice.model.InvalidDateSequenceException;
-import org.killbill.billing.invoice.model.RecurringInvoiceItem;
-import org.killbill.billing.invoice.model.RecurringInvoiceItemData;
-import org.killbill.billing.invoice.model.RecurringInvoiceItemDataWithNextBillingCycleDate;
-import org.killbill.billing.invoice.tree.AccountItemTree;
-import org.killbill.billing.invoice.usage.RawUsageOptimizer;
-import org.killbill.billing.invoice.usage.RawUsageOptimizer.RawUsageOptimizerResult;
-import org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear;
-import org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear.SubscriptionConsumableInArrearItemsAndNextNotificationDate;
-import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.junction.BillingEventSet;
import org.killbill.billing.util.config.InvoiceConfig;
-import org.killbill.billing.util.currency.KillBillMoney;
import org.killbill.clock.Clock;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
import com.google.inject.Inject;
public class DefaultInvoiceGenerator implements InvoiceGenerator {
- private static final Logger log = LoggerFactory.getLogger(DefaultInvoiceGenerator.class);
-
private final Clock clock;
private final InvoiceConfig config;
- private final RawUsageOptimizer rawUsageOptimizer;
- final BillingModeGenerator billingModeGenerator;
+
+ private final FixedAndRecurringInvoiceItemGenerator recurringInvoiceItemGenerator;
+ private final UsageInvoiceItemGenerator usageInvoiceItemGenerator;
@Inject
- public DefaultInvoiceGenerator(final Clock clock, final InvoiceConfig config, final RawUsageOptimizer rawUsageOptimizer) {
+ public DefaultInvoiceGenerator(final Clock clock, final InvoiceConfig config, final FixedAndRecurringInvoiceItemGenerator recurringInvoiceItemGenerator, final UsageInvoiceItemGenerator usageInvoiceItemGenerator) {
this.clock = clock;
this.config = config;
- this.rawUsageOptimizer = rawUsageOptimizer;
- this.billingModeGenerator = new DefaultBillingModeGenerator();
+ this.recurringInvoiceItemGenerator = recurringInvoiceItemGenerator;
+ this.usageInvoiceItemGenerator = usageInvoiceItemGenerator;
}
/*
@@ -110,159 +78,15 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
final UUID invoiceId = invoice.getId();
final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates = new HashMap<UUID, SubscriptionFutureNotificationDates>();
- final List<InvoiceItem> fixedAndRecurringItems = generateFixedAndRecurringInvoiceItems(account.getId(), invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates);
+ final List<InvoiceItem> fixedAndRecurringItems = recurringInvoiceItemGenerator.generateItems(account, invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates, context);
invoice.addInvoiceItems(fixedAndRecurringItems);
- final List<InvoiceItem> usageItems = generateUsageConsumableInArrearItems(account, invoiceId, events, existingInvoices, targetDate, perSubscriptionFutureNotificationDates, context);
+ final List<InvoiceItem> usageItems = usageInvoiceItemGenerator.generateItems(account, invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates, context);
invoice.addInvoiceItems(usageItems);
return new InvoiceWithMetadata(invoice.getInvoiceItems().isEmpty() ? null : invoice, perSubscriptionFutureNotificationDates);
}
- private List<InvoiceItem> generateUsageConsumableInArrearItems(final Account account,
- final UUID invoiceId, final BillingEventSet eventSet,
- @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
- final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates,
- final InternalCallContext internalCallContext) throws InvoiceApiException {
-
- final Map<UUID, List<InvoiceItem>> perSubscriptionConsumableInArrearUsageItems = extractPerSubscriptionExistingConsumableInArrearUsageItems(eventSet.getUsages(), existingInvoices);
- try {
- final List<InvoiceItem> items = Lists.newArrayList();
- final Iterator<BillingEvent> events = eventSet.iterator();
-
- RawUsageOptimizerResult rawUsageOptimizerResult = null;
- List<BillingEvent> curEvents = Lists.newArrayList();
- UUID curSubscriptionId = null;
- while (events.hasNext()) {
- final BillingEvent event = events.next();
- // Skip events that are posterior to the targetDate
- final LocalDate eventLocalEffectiveDate = new LocalDate(event.getEffectiveDate(), event.getAccount().getTimeZone());
- if (eventLocalEffectiveDate.isAfter(targetDate)) {
- continue;
- }
-
- // Optimize to do the usage query only once after we know there are indeed some usage items
- if (rawUsageOptimizerResult == null &&
- Iterables.any(event.getUsages(), new Predicate<Usage>() {
- @Override
- public boolean apply(@Nullable final Usage input) {
- return (input.getUsageType() == UsageType.CONSUMABLE &&
- input.getBillingMode() == BillingMode.IN_ARREAR);
- }
- })) {
- rawUsageOptimizerResult = rawUsageOptimizer.getConsumableInArrearUsage(new LocalDate(event.getEffectiveDate(), account.getTimeZone()), targetDate, Iterables.concat(perSubscriptionConsumableInArrearUsageItems.values()), eventSet.getUsages(), internalCallContext);
- }
-
- // None of the billing events report any usage (CONSUMABLE/IN_ARREAR) sections
- if (rawUsageOptimizerResult == null) {
- continue;
- }
-
- final UUID subscriptionId = event.getSubscription().getId();
- if (curSubscriptionId != null && !curSubscriptionId.equals(subscriptionId)) {
- final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate());
- final List<InvoiceItem> consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId);
-
- final SubscriptionConsumableInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.<InvoiceItem>of());
- final List<InvoiceItem> newInArrearUsageItems = subscriptionResult.getInvoiceItems();
- items.addAll(newInArrearUsageItems);
- updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, subscriptionResult.getPerUsageNotificationDates(), BillingMode.IN_ARREAR, perSubscriptionFutureNotificationDates);
- curEvents = Lists.newArrayList();
- }
- curSubscriptionId = subscriptionId;
- curEvents.add(event);
- }
- if (curSubscriptionId != null) {
- final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate());
- final List<InvoiceItem> consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId);
-
- final SubscriptionConsumableInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.<InvoiceItem>of());
- final List<InvoiceItem> newInArrearUsageItems = subscriptionResult.getInvoiceItems();
- items.addAll(newInArrearUsageItems);
- updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, subscriptionResult.getPerUsageNotificationDates(), BillingMode.IN_ARREAR, perSubscriptionFutureNotificationDates);
- }
- return items;
-
- } catch (CatalogApiException e) {
- throw new InvoiceApiException(e);
- }
- }
-
- private void updatePerSubscriptionNextNotificationUsageDate(final UUID subscriptionId, final Map<String, LocalDate> nextBillingCycleDates, final BillingMode usageBillingMode, final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates) {
- if (usageBillingMode == BillingMode.IN_ADVANCE) {
- throw new IllegalStateException("Not implemented Yet)");
- }
-
- SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates.get(subscriptionId);
- if (subscriptionFutureNotificationDates == null) {
- subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates(null);
- perSubscriptionFutureNotificationDates.put(subscriptionId, subscriptionFutureNotificationDates);
- }
- for (final String usageName : nextBillingCycleDates.keySet()) {
- subscriptionFutureNotificationDates.updateNextUsageDateIfRequired(usageName, usageBillingMode, nextBillingCycleDates.get(usageName));
- }
- }
-
- private Map<UUID, List<InvoiceItem>> extractPerSubscriptionExistingConsumableInArrearUsageItems(final Map<String, Usage> knownUsage, @Nullable final List<Invoice> existingInvoices) {
-
- if (existingInvoices == null || existingInvoices.isEmpty()) {
- return ImmutableMap.of();
- }
-
- final Map<UUID, List<InvoiceItem>> result = new HashMap<UUID, List<InvoiceItem>>();
- final Iterable<InvoiceItem> usageConsumableInArrearItems = Iterables.concat(Iterables.transform(existingInvoices, new Function<Invoice, Iterable<InvoiceItem>>() {
- @Override
- public Iterable<InvoiceItem> apply(final Invoice input) {
-
- return Iterables.filter(input.getInvoiceItems(), new Predicate<InvoiceItem>() {
- @Override
- public boolean apply(final InvoiceItem input) {
- if (input.getInvoiceItemType() == InvoiceItemType.USAGE) {
- final Usage usage = knownUsage.get(input.getUsageName());
- return usage.getUsageType() == UsageType.CONSUMABLE && usage.getBillingMode() == BillingMode.IN_ARREAR;
- }
- return false;
- }
- });
- }
- }));
-
- for (InvoiceItem cur : usageConsumableInArrearItems) {
- List<InvoiceItem> perSubscriptionUsageItems = result.get(cur.getSubscriptionId());
- if (perSubscriptionUsageItems == null) {
- perSubscriptionUsageItems = new LinkedList<InvoiceItem>();
- result.put(cur.getSubscriptionId(), perSubscriptionUsageItems);
- }
- perSubscriptionUsageItems.add(cur);
- }
- return result;
- }
-
- private List<InvoiceItem> generateFixedAndRecurringInvoiceItems(final UUID accountId, final UUID invoiceId, final BillingEventSet eventSet,
- @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
- final Currency targetCurrency, Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate) throws InvoiceApiException {
- final AccountItemTree accountItemTree = new AccountItemTree(accountId, invoiceId);
- 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.
- !eventSet.getSubscriptionIdsWithAutoInvoiceOff()
- .contains(item.getSubscriptionId())) { //don't add items with auto_invoice_off tag
- accountItemTree.addExistingItem(item);
- }
- }
- }
- }
-
- // 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 = new ArrayList<InvoiceItem>();
- processRecurringBillingEvents(invoiceId, accountId, eventSet, targetDate, targetCurrency, proposedItems, perSubscriptionFutureNotificationDate);
- processFixedBillingEvents(invoiceId, accountId, eventSet, targetDate, targetCurrency, proposedItems);
-
- accountItemTree.mergeWithProposedItems(proposedItems);
- return accountItemTree.getResultingItemList();
- }
-
private void validateTargetDate(final LocalDate targetDate) throws InvoiceApiException {
final int maximumNumberOfMonths = config.getNumberOfMonthsInFuture();
@@ -285,162 +109,4 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
}
return maxDate;
}
-
- private List<InvoiceItem> processRecurringBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events,
- final LocalDate targetDate, final Currency currency, final List<InvoiceItem> proposedItems,
- final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate) throws InvoiceApiException {
-
- if (events.size() == 0) {
- return proposedItems;
- }
-
- // Pretty-print the generated invoice items from the junction events
- final StringBuilder logStringBuilder = new StringBuilder("Proposed Invoice items for invoiceId ")
- .append(invoiceId)
- .append(" and accountId ")
- .append(accountId);
-
- final Iterator<BillingEvent> eventIt = events.iterator();
- BillingEvent nextEvent = eventIt.next();
- while (eventIt.hasNext()) {
- final BillingEvent thisEvent = nextEvent;
- nextEvent = eventIt.next();
- if (!events.getSubscriptionIdsWithAutoInvoiceOff().
- contains(thisEvent.getSubscription().getId())) { // don't consider events for subscriptions that have auto_invoice_off
- final BillingEvent adjustedNextEvent = (thisEvent.getSubscription().getId() == nextEvent.getSubscription().getId()) ? nextEvent : null;
- final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, thisEvent, adjustedNextEvent, targetDate, currency, logStringBuilder, events.getRecurringBillingMode(), perSubscriptionFutureNotificationDate);
- proposedItems.addAll(newProposedItems);
- }
- }
- final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, nextEvent, null, targetDate, currency, logStringBuilder, events.getRecurringBillingMode(), perSubscriptionFutureNotificationDate);
- proposedItems.addAll(newProposedItems);
-
- log.info(logStringBuilder.toString());
-
- return proposedItems;
- }
-
- private List<InvoiceItem> processFixedBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events, final LocalDate targetDate, final Currency currency, final List<InvoiceItem> proposedItems) {
- final Iterator<BillingEvent> eventIt = events.iterator();
- while (eventIt.hasNext()) {
- final BillingEvent thisEvent = eventIt.next();
-
- final InvoiceItem fixedPriceInvoiceItem = generateFixedPriceItem(invoiceId, accountId, thisEvent, targetDate, currency);
- if (fixedPriceInvoiceItem != null) {
- proposedItems.add(fixedPriceInvoiceItem);
- }
- }
- return proposedItems;
- }
-
- // Turn a set of events into a list of invoice items. Note that the dates on the invoice items will be rounded (granularity of a day)
- private List<InvoiceItem> processRecurringEvent(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent, @Nullable final BillingEvent nextEvent,
- final LocalDate targetDate, final Currency currency,
- final StringBuilder logStringBuilder, final BillingMode billingMode,
- final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate) throws InvoiceApiException {
- final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
-
- // Handle recurring items
- final BillingPeriod billingPeriod = thisEvent.getBillingPeriod();
- if (billingPeriod != BillingPeriod.NO_BILLING_PERIOD) {
- final LocalDate startDate = new LocalDate(thisEvent.getEffectiveDate(), thisEvent.getTimeZone());
-
- if (!startDate.isAfter(targetDate)) {
- final LocalDate endDate = (nextEvent == null) ? null : new LocalDate(nextEvent.getEffectiveDate(), nextEvent.getTimeZone());
-
- final int billCycleDayLocal = thisEvent.getBillCycleDayLocal();
-
- final RecurringInvoiceItemDataWithNextBillingCycleDate itemDataWithNextBillingCycleDate;
- try {
- itemDataWithNextBillingCycleDate = billingModeGenerator.generateInvoiceItemData(startDate, endDate, targetDate, billCycleDayLocal, billingPeriod, billingMode);
- } catch (InvalidDateSequenceException e) {
- throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_DATE_SEQUENCE, startDate, endDate, targetDate);
- }
-
- // STEPH Move all this logic into RecurringInvoiceItemData
- for (final RecurringInvoiceItemData itemDatum : itemDataWithNextBillingCycleDate.getItemData()) {
- final BigDecimal rate = thisEvent.getRecurringPrice();
-
- if (rate != null) {
- final BigDecimal amount = KillBillMoney.of(itemDatum.getNumberOfCycles().multiply(rate), currency);
-
- final RecurringInvoiceItem recurringItem = new RecurringInvoiceItem(invoiceId,
- accountId,
- thisEvent.getSubscription().getBundleId(),
- thisEvent.getSubscription().getId(),
- thisEvent.getPlan().getName(),
- thisEvent.getPlanPhase().getName(),
- itemDatum.getStartDate(), itemDatum.getEndDate(),
- amount, rate, currency);
- items.add(recurringItem);
- }
- }
- updatePerSubscriptionNextNotificationDate(thisEvent.getSubscription().getId(), itemDataWithNextBillingCycleDate.getNextBillingCycleDate(), items, billingMode, perSubscriptionFutureNotificationDate);
-
- }
- }
-
- // For debugging purposes
- logStringBuilder.append("\n")
- .append(thisEvent);
- for (final InvoiceItem item : items) {
- logStringBuilder.append("\n\t")
- .append(item);
- }
- return items;
- }
-
- private void updatePerSubscriptionNextNotificationDate(final UUID subscriptionId, final LocalDate nextBillingCycleDate, final List<InvoiceItem> newProposedItems, final BillingMode billingMode, final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates) {
-
- LocalDate nextNotificationDate = null;
- switch (billingMode) {
- case IN_ADVANCE:
- for (final InvoiceItem item : newProposedItems) {
- if ((item.getEndDate() != null) &&
- (item.getAmount() == null ||
- item.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
- if (nextNotificationDate == null) {
- nextNotificationDate = item.getEndDate();
- } else {
- nextNotificationDate = nextNotificationDate.compareTo(item.getEndDate()) > 0 ? nextNotificationDate : item.getEndDate();
- }
- }
- }
- break;
- case IN_ARREAR:
- nextNotificationDate = nextBillingCycleDate;
- break;
- default:
- throw new IllegalStateException("Unrecognized billing mode " + billingMode);
- }
- if (nextNotificationDate != null) {
- SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates.get(subscriptionId);
- if (subscriptionFutureNotificationDates == null) {
- subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates(billingMode);
- perSubscriptionFutureNotificationDates.put(subscriptionId, subscriptionFutureNotificationDates);
- }
- subscriptionFutureNotificationDates.updateNextRecurringDateIfRequired(nextNotificationDate);
-
- }
- }
-
- private InvoiceItem generateFixedPriceItem(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent,
- final LocalDate targetDate, final Currency currency) {
- final LocalDate roundedStartDate = new LocalDate(thisEvent.getEffectiveDate(), thisEvent.getTimeZone());
-
- if (roundedStartDate.isAfter(targetDate)) {
- return null;
- } else {
- final BigDecimal fixedPrice = thisEvent.getFixedPrice();
-
- if (fixedPrice != null) {
- return new FixedPriceInvoiceItem(invoiceId, accountId, thisEvent.getSubscription().getBundleId(),
- thisEvent.getSubscription().getId(),
- thisEvent.getPlan().getName(), thisEvent.getPlanPhase().getName(),
- roundedStartDate, fixedPrice, currency);
- } else {
- return null;
- }
- }
- }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
new file mode 100644
index 0000000..19ceeee
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.generator;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.BillingMode;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
+import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.model.RecurringInvoiceItem;
+import org.killbill.billing.invoice.model.RecurringInvoiceItemData;
+import org.killbill.billing.invoice.model.RecurringInvoiceItemDataWithNextBillingCycleDate;
+import org.killbill.billing.invoice.tree.AccountItemTree;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingEventSet;
+import org.killbill.billing.util.currency.KillBillMoney;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Inject;
+
+import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateNumberOfWholeBillingPeriods;
+import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateProRationAfterLastBillingCycleDate;
+import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateProRationBeforeFirstBillingPeriod;
+
+public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator {
+
+ private static final Logger log = LoggerFactory.getLogger(FixedAndRecurringInvoiceItemGenerator.class);
+
+ @Inject
+ public FixedAndRecurringInvoiceItemGenerator() {
+ }
+
+ public List<InvoiceItem> generateItems(final Account account, final UUID invoiceId, final BillingEventSet eventSet,
+ @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
+ final Currency targetCurrency, Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate,
+ final InternalCallContext internalCallContext) throws InvoiceApiException {
+ final AccountItemTree accountItemTree = new AccountItemTree(account.getId(), invoiceId);
+ 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.
+ !eventSet.getSubscriptionIdsWithAutoInvoiceOff()
+ .contains(item.getSubscriptionId())) { //don't add items with auto_invoice_off tag
+ accountItemTree.addExistingItem(item);
+ }
+ }
+ }
+ }
+
+ // 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 = new ArrayList<InvoiceItem>();
+ processRecurringBillingEvents(invoiceId, account.getId(), eventSet, targetDate, targetCurrency, proposedItems, perSubscriptionFutureNotificationDate);
+ processFixedBillingEvents(invoiceId, account.getId(), eventSet, targetDate, targetCurrency, proposedItems);
+
+ accountItemTree.mergeWithProposedItems(proposedItems);
+ return accountItemTree.getResultingItemList();
+ }
+
+ private List<InvoiceItem> processRecurringBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events,
+ final LocalDate targetDate, final Currency currency, final List<InvoiceItem> proposedItems,
+ final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate) throws InvoiceApiException {
+
+ if (events.size() == 0) {
+ return proposedItems;
+ }
+
+ // Pretty-print the generated invoice items from the junction events
+ final StringBuilder logStringBuilder = new StringBuilder("Proposed Invoice items for invoiceId ")
+ .append(invoiceId)
+ .append(" and accountId ")
+ .append(accountId);
+
+ final Iterator<BillingEvent> eventIt = events.iterator();
+ BillingEvent nextEvent = eventIt.next();
+ while (eventIt.hasNext()) {
+ final BillingEvent thisEvent = nextEvent;
+ nextEvent = eventIt.next();
+ if (!events.getSubscriptionIdsWithAutoInvoiceOff().
+ contains(thisEvent.getSubscription().getId())) { // don't consider events for subscriptions that have auto_invoice_off
+ final BillingEvent adjustedNextEvent = (thisEvent.getSubscription().getId() == nextEvent.getSubscription().getId()) ? nextEvent : null;
+ final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, thisEvent, adjustedNextEvent, targetDate, currency, logStringBuilder, events.getRecurringBillingMode(), perSubscriptionFutureNotificationDate);
+ proposedItems.addAll(newProposedItems);
+ }
+ }
+ final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, nextEvent, null, targetDate, currency, logStringBuilder, events.getRecurringBillingMode(), perSubscriptionFutureNotificationDate);
+ proposedItems.addAll(newProposedItems);
+
+ log.info(logStringBuilder.toString());
+
+ return proposedItems;
+ }
+
+ private List<InvoiceItem> processFixedBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events, final LocalDate targetDate, final Currency currency, final List<InvoiceItem> proposedItems) {
+ final Iterator<BillingEvent> eventIt = events.iterator();
+ while (eventIt.hasNext()) {
+ final BillingEvent thisEvent = eventIt.next();
+
+ final InvoiceItem fixedPriceInvoiceItem = generateFixedPriceItem(invoiceId, accountId, thisEvent, targetDate, currency);
+ if (fixedPriceInvoiceItem != null) {
+ proposedItems.add(fixedPriceInvoiceItem);
+ }
+ }
+ return proposedItems;
+ }
+
+ // Turn a set of events into a list of invoice items. Note that the dates on the invoice items will be rounded (granularity of a day)
+ private List<InvoiceItem> processRecurringEvent(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent, @Nullable final BillingEvent nextEvent,
+ final LocalDate targetDate, final Currency currency,
+ final StringBuilder logStringBuilder, final BillingMode billingMode,
+ final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate) throws InvoiceApiException {
+ final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
+
+ // Handle recurring items
+ final BillingPeriod billingPeriod = thisEvent.getBillingPeriod();
+ if (billingPeriod != BillingPeriod.NO_BILLING_PERIOD) {
+ final LocalDate startDate = new LocalDate(thisEvent.getEffectiveDate(), thisEvent.getTimeZone());
+
+ if (!startDate.isAfter(targetDate)) {
+ final LocalDate endDate = (nextEvent == null) ? null : new LocalDate(nextEvent.getEffectiveDate(), nextEvent.getTimeZone());
+
+ final int billCycleDayLocal = thisEvent.getBillCycleDayLocal();
+
+ final RecurringInvoiceItemDataWithNextBillingCycleDate itemDataWithNextBillingCycleDate;
+ try {
+ itemDataWithNextBillingCycleDate = generateInvoiceItemData(startDate, endDate, targetDate, billCycleDayLocal, billingPeriod, billingMode);
+ } catch (InvalidDateSequenceException e) {
+ throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_DATE_SEQUENCE, startDate, endDate, targetDate);
+ }
+
+ for (final RecurringInvoiceItemData itemDatum : itemDataWithNextBillingCycleDate.getItemData()) {
+ final BigDecimal rate = thisEvent.getRecurringPrice();
+
+ if (rate != null) {
+ final BigDecimal amount = KillBillMoney.of(itemDatum.getNumberOfCycles().multiply(rate), currency);
+
+ final RecurringInvoiceItem recurringItem = new RecurringInvoiceItem(invoiceId,
+ accountId,
+ thisEvent.getSubscription().getBundleId(),
+ thisEvent.getSubscription().getId(),
+ thisEvent.getPlan().getName(),
+ thisEvent.getPlanPhase().getName(),
+ itemDatum.getStartDate(), itemDatum.getEndDate(),
+ amount, rate, currency);
+ items.add(recurringItem);
+ }
+ }
+ updatePerSubscriptionNextNotificationDate(thisEvent.getSubscription().getId(), itemDataWithNextBillingCycleDate.getNextBillingCycleDate(), items, billingMode, perSubscriptionFutureNotificationDate);
+
+ }
+ }
+
+ // For debugging purposes
+ logStringBuilder.append("\n")
+ .append(thisEvent);
+ for (final InvoiceItem item : items) {
+ logStringBuilder.append("\n\t")
+ .append(item);
+ }
+ return items;
+ }
+
+ private void updatePerSubscriptionNextNotificationDate(final UUID subscriptionId, final LocalDate nextBillingCycleDate, final List<InvoiceItem> newProposedItems, final BillingMode billingMode, final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates) {
+
+ LocalDate nextNotificationDate = null;
+ switch (billingMode) {
+ case IN_ADVANCE:
+ for (final InvoiceItem item : newProposedItems) {
+ if ((item.getEndDate() != null) &&
+ (item.getAmount() == null ||
+ item.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
+ if (nextNotificationDate == null) {
+ nextNotificationDate = item.getEndDate();
+ } else {
+ nextNotificationDate = nextNotificationDate.compareTo(item.getEndDate()) > 0 ? nextNotificationDate : item.getEndDate();
+ }
+ }
+ }
+ break;
+ case IN_ARREAR:
+ nextNotificationDate = nextBillingCycleDate;
+ break;
+ default:
+ throw new IllegalStateException("Unrecognized billing mode " + billingMode);
+ }
+ if (nextNotificationDate != null) {
+ SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates.get(subscriptionId);
+ if (subscriptionFutureNotificationDates == null) {
+ subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates(billingMode);
+ perSubscriptionFutureNotificationDates.put(subscriptionId, subscriptionFutureNotificationDates);
+ }
+ subscriptionFutureNotificationDates.updateNextRecurringDateIfRequired(nextNotificationDate);
+
+ }
+ }
+
+ public RecurringInvoiceItemDataWithNextBillingCycleDate generateInvoiceItemData(final LocalDate startDate, @Nullable final LocalDate endDate,
+ final LocalDate targetDate,
+ final int billingCycleDayLocal,
+ final BillingPeriod billingPeriod,
+ final BillingMode billingMode) throws InvalidDateSequenceException {
+ if (endDate != null && endDate.isBefore(startDate)) {
+ throw new InvalidDateSequenceException();
+ }
+ if (targetDate.isBefore(startDate)) {
+ throw new InvalidDateSequenceException();
+ }
+
+ final List<RecurringInvoiceItemData> results = new ArrayList<RecurringInvoiceItemData>();
+
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(startDate, endDate, targetDate, billingCycleDayLocal, billingPeriod, billingMode);
+
+ // We are not billing for less than a day
+ if (!billingIntervalDetail.hasSomethingToBill()) {
+ return new RecurringInvoiceItemDataWithNextBillingCycleDate(results, billingIntervalDetail);
+ }
+ //
+ // If there is an endDate and that endDate is before our first coming firstBillingCycleDate, all we have to do
+ // is to charge for that period
+ //
+ if (endDate != null && !endDate.isAfter(billingIntervalDetail.getFirstBillingCycleDate())) {
+ final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, endDate, billingPeriod);
+ final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, endDate, leadingProRationPeriods);
+ results.add(itemData);
+ return new RecurringInvoiceItemDataWithNextBillingCycleDate(results, billingIntervalDetail);
+ }
+
+ //
+ // Leading proration if
+ // i) The first firstBillingCycleDate is strictly after our start date AND
+ // ii) The endDate is is not null and is strictly after our firstBillingCycleDate (previous check)
+ //
+ if (billingIntervalDetail.getFirstBillingCycleDate().isAfter(startDate)) {
+ final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, billingIntervalDetail.getFirstBillingCycleDate(), billingPeriod);
+ if (leadingProRationPeriods != null && leadingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) {
+ // Not common - add info in the logs for debugging purposes
+ final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, billingIntervalDetail.getFirstBillingCycleDate(), leadingProRationPeriods);
+ log.info("Adding pro-ration: {}", itemData);
+ results.add(itemData);
+ }
+ }
+
+ //
+ // Calculate the effectiveEndDate from the firstBillingCycleDate:
+ // - If endDate != null and targetDate is after endDate => this is the endDate and will lead to a trailing pro-ration
+ // - If not, this is the last billingCycleDate calculation right after the targetDate
+ //
+ final LocalDate effectiveEndDate = billingIntervalDetail.getEffectiveEndDate();
+
+ //
+ // Based on what we calculated previously, code recompute one more time the numberOfWholeBillingPeriods
+ //
+ final LocalDate lastBillingCycleDate = billingIntervalDetail.getLastBillingCycleDate();
+ final int numberOfWholeBillingPeriods = calculateNumberOfWholeBillingPeriods(billingIntervalDetail.getFirstBillingCycleDate(), lastBillingCycleDate, billingPeriod);
+
+ for (int i = 0; i < numberOfWholeBillingPeriods; i++) {
+ final LocalDate servicePeriodStartDate;
+ if (results.size() > 0) {
+ // Make sure the periods align, especially with the pro-ration calculations above
+ servicePeriodStartDate = results.get(results.size() - 1).getEndDate();
+ } else if (i == 0) {
+ // Use the specified start date
+ servicePeriodStartDate = startDate;
+ } else {
+ throw new IllegalStateException("We should at least have one invoice item!");
+ }
+
+ // Make sure to align the end date with the BCD
+ final LocalDate servicePeriodEndDate = billingIntervalDetail.getFutureBillingDateFor(i + 1);
+ results.add(new RecurringInvoiceItemData(servicePeriodStartDate, servicePeriodEndDate, BigDecimal.ONE));
+ }
+
+ //
+ // Now we check if indeed we need a trailing proration and add that incomplete item
+ //
+ if (effectiveEndDate.isAfter(lastBillingCycleDate)) {
+ final BigDecimal trailingProRationPeriods = calculateProRationAfterLastBillingCycleDate(effectiveEndDate, lastBillingCycleDate, billingPeriod);
+ if (trailingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) {
+ // Not common - add info in the logs for debugging purposes
+ final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(lastBillingCycleDate, effectiveEndDate, trailingProRationPeriods);
+ log.info("Adding trailing pro-ration: {}", itemData);
+ results.add(itemData);
+ }
+ }
+ return new RecurringInvoiceItemDataWithNextBillingCycleDate(results, billingIntervalDetail);
+ }
+
+ private InvoiceItem generateFixedPriceItem(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent,
+ final LocalDate targetDate, final Currency currency) {
+ final LocalDate roundedStartDate = new LocalDate(thisEvent.getEffectiveDate(), thisEvent.getTimeZone());
+ if (roundedStartDate.isAfter(targetDate)) {
+ return null;
+ } else {
+ final BigDecimal fixedPrice = thisEvent.getFixedPrice();
+
+ if (fixedPrice != null) {
+ return new FixedPriceInvoiceItem(invoiceId, accountId, thisEvent.getSubscription().getBundleId(),
+ thisEvent.getSubscription().getId(),
+ thisEvent.getPlan().getName(), thisEvent.getPlanPhase().getName(),
+ roundedStartDate, fixedPrice, currency);
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java
index 3ff29a6..09f41ee 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java
@@ -31,7 +31,6 @@ import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.junction.BillingEventSet;
public interface InvoiceGenerator {
-
- public InvoiceWithMetadata generateInvoice(Account account, @Nullable BillingEventSet events, @Nullable List<Invoice> existingInvoices,
- LocalDate targetDate, Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException;
+ InvoiceWithMetadata generateInvoice(Account account, @Nullable BillingEventSet events, @Nullable List<Invoice> existingInvoices,
+ LocalDate targetDate, Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException;
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java
new file mode 100644
index 0000000..6009549
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.generator;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
+import org.killbill.billing.junction.BillingEventSet;
+
+public abstract class InvoiceItemGenerator {
+
+ public abstract List<InvoiceItem> generateItems(final Account account, final UUID invoiceId, final BillingEventSet eventSet,
+ @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
+ final Currency targetCurrency, Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate,
+ final InternalCallContext context) throws InvoiceApiException;
+
+
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java
new file mode 100644
index 0000000..7750be8
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.generator;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.BillingMode;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.Usage;
+import org.killbill.billing.catalog.api.UsageType;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
+import org.killbill.billing.invoice.usage.RawUsageOptimizer;
+import org.killbill.billing.invoice.usage.RawUsageOptimizer.RawUsageOptimizerResult;
+import org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear;
+import org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear.SubscriptionConsumableInArrearItemsAndNextNotificationDate;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingEventSet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+
+public class UsageInvoiceItemGenerator extends InvoiceItemGenerator {
+
+ private static final Logger log = LoggerFactory.getLogger(UsageInvoiceItemGenerator.class);
+
+ private final RawUsageOptimizer rawUsageOptimizer;
+
+ @Inject
+ public UsageInvoiceItemGenerator(final RawUsageOptimizer rawUsageOptimizer) {
+ this.rawUsageOptimizer = rawUsageOptimizer;
+ }
+
+
+ @Override
+ public List<InvoiceItem> generateItems(final Account account,
+ final UUID invoiceId,
+ final BillingEventSet eventSet,
+ @Nullable final List<Invoice> existingInvoices,
+ final LocalDate targetDate,
+ final Currency targetCurrency,
+ final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates,
+ final InternalCallContext internalCallContext) throws InvoiceApiException {
+
+ final Map<UUID, List<InvoiceItem>> perSubscriptionConsumableInArrearUsageItems = extractPerSubscriptionExistingConsumableInArrearUsageItems(eventSet.getUsages(), existingInvoices);
+ try {
+ final List<InvoiceItem> items = Lists.newArrayList();
+ final Iterator<BillingEvent> events = eventSet.iterator();
+
+ RawUsageOptimizerResult rawUsageOptimizerResult = null;
+ List<BillingEvent> curEvents = Lists.newArrayList();
+ UUID curSubscriptionId = null;
+ while (events.hasNext()) {
+ final BillingEvent event = events.next();
+ // Skip events that are posterior to the targetDate
+ final LocalDate eventLocalEffectiveDate = new LocalDate(event.getEffectiveDate(), event.getAccount().getTimeZone());
+ if (eventLocalEffectiveDate.isAfter(targetDate)) {
+ continue;
+ }
+
+ // Optimize to do the usage query only once after we know there are indeed some usage items
+ if (rawUsageOptimizerResult == null &&
+ Iterables.any(event.getUsages(), new Predicate<Usage>() {
+ @Override
+ public boolean apply(@Nullable final Usage input) {
+ return (input.getUsageType() == UsageType.CONSUMABLE &&
+ input.getBillingMode() == BillingMode.IN_ARREAR);
+ }
+ })) {
+ rawUsageOptimizerResult = rawUsageOptimizer.getConsumableInArrearUsage(new LocalDate(event.getEffectiveDate(), account.getTimeZone()), targetDate, Iterables.concat(perSubscriptionConsumableInArrearUsageItems.values()), eventSet.getUsages(), internalCallContext);
+ }
+
+ // None of the billing events report any usage (CONSUMABLE/IN_ARREAR) sections
+ if (rawUsageOptimizerResult == null) {
+ continue;
+ }
+
+ final UUID subscriptionId = event.getSubscription().getId();
+ if (curSubscriptionId != null && !curSubscriptionId.equals(subscriptionId)) {
+ final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate());
+ final List<InvoiceItem> consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId);
+
+ final SubscriptionConsumableInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.<InvoiceItem>of());
+ final List<InvoiceItem> newInArrearUsageItems = subscriptionResult.getInvoiceItems();
+ items.addAll(newInArrearUsageItems);
+ updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, subscriptionResult.getPerUsageNotificationDates(), BillingMode.IN_ARREAR, perSubscriptionFutureNotificationDates);
+ curEvents = Lists.newArrayList();
+ }
+ curSubscriptionId = subscriptionId;
+ curEvents.add(event);
+ }
+ if (curSubscriptionId != null) {
+ final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate());
+ final List<InvoiceItem> consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId);
+
+ final SubscriptionConsumableInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.<InvoiceItem>of());
+ final List<InvoiceItem> newInArrearUsageItems = subscriptionResult.getInvoiceItems();
+ items.addAll(newInArrearUsageItems);
+ updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, subscriptionResult.getPerUsageNotificationDates(), BillingMode.IN_ARREAR, perSubscriptionFutureNotificationDates);
+ }
+ return items;
+
+ } catch (CatalogApiException e) {
+ throw new InvoiceApiException(e);
+ }
+ }
+
+ private void updatePerSubscriptionNextNotificationUsageDate(final UUID subscriptionId, final Map<String, LocalDate> nextBillingCycleDates, final BillingMode usageBillingMode, final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates) {
+ if (usageBillingMode == BillingMode.IN_ADVANCE) {
+ throw new IllegalStateException("Not implemented Yet)");
+ }
+
+ SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates.get(subscriptionId);
+ if (subscriptionFutureNotificationDates == null) {
+ subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates(null);
+ perSubscriptionFutureNotificationDates.put(subscriptionId, subscriptionFutureNotificationDates);
+ }
+ for (final String usageName : nextBillingCycleDates.keySet()) {
+ subscriptionFutureNotificationDates.updateNextUsageDateIfRequired(usageName, usageBillingMode, nextBillingCycleDates.get(usageName));
+ }
+ }
+
+ private Map<UUID, List<InvoiceItem>> extractPerSubscriptionExistingConsumableInArrearUsageItems(final Map<String, Usage> knownUsage, @Nullable final List<Invoice> existingInvoices) {
+
+ if (existingInvoices == null || existingInvoices.isEmpty()) {
+ return ImmutableMap.of();
+ }
+
+ final Map<UUID, List<InvoiceItem>> result = new HashMap<UUID, List<InvoiceItem>>();
+ final Iterable<InvoiceItem> usageConsumableInArrearItems = Iterables.concat(Iterables.transform(existingInvoices, new Function<Invoice, Iterable<InvoiceItem>>() {
+ @Override
+ public Iterable<InvoiceItem> apply(final Invoice input) {
+
+ return Iterables.filter(input.getInvoiceItems(), new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ if (input.getInvoiceItemType() == InvoiceItemType.USAGE) {
+ final Usage usage = knownUsage.get(input.getUsageName());
+ return usage.getUsageType() == UsageType.CONSUMABLE && usage.getBillingMode() == BillingMode.IN_ARREAR;
+ }
+ return false;
+ }
+ });
+ }
+ }));
+
+ for (InvoiceItem cur : usageConsumableInArrearItems) {
+ List<InvoiceItem> perSubscriptionUsageItems = result.get(cur.getSubscriptionId());
+ if (perSubscriptionUsageItems == null) {
+ perSubscriptionUsageItems = new LinkedList<InvoiceItem>();
+ result.put(cur.getSubscriptionId(), perSubscriptionUsageItems);
+ }
+ perSubscriptionUsageItems.add(cur);
+ }
+ return result;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java b/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java
index f106da5..1973ff2 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java
@@ -39,7 +39,9 @@ import org.killbill.billing.invoice.api.user.DefaultInvoiceUserApi;
import org.killbill.billing.invoice.dao.DefaultInvoiceDao;
import org.killbill.billing.invoice.dao.InvoiceDao;
import org.killbill.billing.invoice.generator.DefaultInvoiceGenerator;
+import org.killbill.billing.invoice.generator.FixedAndRecurringInvoiceItemGenerator;
import org.killbill.billing.invoice.generator.InvoiceGenerator;
+import org.killbill.billing.invoice.generator.UsageInvoiceItemGenerator;
import org.killbill.billing.invoice.notification.DefaultNextBillingDateNotifier;
import org.killbill.billing.invoice.notification.DefaultNextBillingDatePoster;
import org.killbill.billing.invoice.notification.EmailInvoiceNotifier;
@@ -133,6 +135,8 @@ public class DefaultInvoiceModule extends KillBillModule implements InvoiceModul
protected void installInvoiceGenerator() {
bind(InvoiceGenerator.class).to(DefaultInvoiceGenerator.class).asEagerSingleton();
+ bind(FixedAndRecurringInvoiceItemGenerator.class).asEagerSingleton();
+ bind(UsageInvoiceItemGenerator.class).asEagerSingleton();
}
protected void installInvoicePluginApi() {
@@ -157,7 +161,8 @@ public class DefaultInvoiceModule extends KillBillModule implements InvoiceModul
installInvoicePaymentApi();
installInvoiceMigrationApi();
installResourceBundleFactory();
- bind(RawUsageOptimizer.class).asEagerSingleton();;
+ bind(RawUsageOptimizer.class).asEagerSingleton();
+ ;
bind(InvoiceApiHelper.class).asEagerSingleton();
}
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index 9c5ec23..4d45ea4 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -133,7 +133,6 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
return 10;
}
};
- this.generator = new DefaultInvoiceGenerator(clock, invoiceConfig, null);
this.account = new MockAccountBuilder().name(UUID.randomUUID().toString().substring(1, 8))
.firstNameLength(6)
.email(UUID.randomUUID().toString().substring(1, 8))
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
index 1069bfc..2e2692e 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
@@ -27,6 +27,7 @@ import org.killbill.billing.invoice.api.InvoicePaymentApi;
import org.killbill.billing.invoice.api.InvoiceUserApi;
import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
import org.killbill.billing.invoice.dao.InvoiceDao;
+import org.killbill.billing.invoice.generator.FixedAndRecurringInvoiceItemGenerator;
import org.killbill.billing.invoice.generator.InvoiceGenerator;
import org.killbill.billing.invoice.glue.TestInvoiceModuleNoDB;
import org.killbill.billing.invoice.usage.RawUsageOptimizer;
@@ -97,7 +98,8 @@ public abstract class InvoiceTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
protected ResourceBundleFactory resourceBundleFactory;
@Inject
protected RawUsageOptimizer rawUsageOptimizer;
-
+ @Inject
+ protected FixedAndRecurringInvoiceItemGenerator fixedAndRecurringInvoiceItemGenerator;
@Override
protected KillbillConfigSource getConfigSource() {
return getConfigSource("/resource.properties");
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/TestValidationProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/TestValidationProRation.java
index 471b9f0..ea1fd4b 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/TestValidationProRation.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/TestValidationProRation.java
@@ -18,11 +18,9 @@ package org.killbill.billing.invoice.proRations.inAdvance;
import org.joda.time.LocalDate;
import org.killbill.billing.catalog.api.BillingMode;
-import org.killbill.billing.invoice.model.BillingModeGenerator;
import org.testng.annotations.Test;
import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.invoice.model.DefaultBillingModeGenerator;
import org.killbill.billing.invoice.model.InvalidDateSequenceException;
import org.killbill.billing.invoice.proRations.ProRationTestBase;
@@ -40,11 +38,6 @@ public class TestValidationProRation extends ProRationTestBase {
return BillingMode.IN_ADVANCE;
}
- @Override
- protected BillingModeGenerator getBillingModeGenerator() {
- return new DefaultBillingModeGenerator();
- }
-
@Test(groups = "fast", expectedExceptions = InvalidDateSequenceException.class)
public void testTargetStartEnd() throws InvalidDateSequenceException {
final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 30);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/proRations/ProRationTestBase.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/ProRationTestBase.java
index 2a31e14..14c5275 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/proRations/ProRationTestBase.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/ProRationTestBase.java
@@ -19,15 +19,12 @@ package org.killbill.billing.invoice.proRations;
import static org.killbill.billing.invoice.TestInvoiceHelper.*;
import java.math.BigDecimal;
-import java.util.List;
import org.joda.time.LocalDate;
import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
-import org.killbill.billing.invoice.model.BillingModeGenerator;
-import org.killbill.billing.invoice.model.DefaultBillingModeGenerator;
import org.killbill.billing.invoice.model.InvalidDateSequenceException;
import org.killbill.billing.invoice.model.RecurringInvoiceItemData;
import org.killbill.billing.invoice.model.RecurringInvoiceItemDataWithNextBillingCycleDate;
@@ -37,10 +34,6 @@ import static org.testng.Assert.fail;
public abstract class ProRationTestBase extends InvoiceTestSuiteNoDB {
- protected BillingModeGenerator getBillingModeGenerator() {
- return new DefaultBillingModeGenerator();
- }
-
protected abstract BillingPeriod getBillingPeriod();
protected abstract BillingMode getBillingMode();
@@ -73,7 +66,7 @@ public abstract class ProRationTestBase extends InvoiceTestSuiteNoDB {
}
protected BigDecimal calculateNumberOfBillingCycles(final LocalDate startDate, final LocalDate endDate, final LocalDate targetDate, final int billingCycleDay) throws InvalidDateSequenceException {
- final RecurringInvoiceItemDataWithNextBillingCycleDate items = getBillingModeGenerator().generateInvoiceItemData(startDate, endDate, targetDate, billingCycleDay, getBillingPeriod(), getBillingMode());
+ final RecurringInvoiceItemDataWithNextBillingCycleDate items = fixedAndRecurringInvoiceItemGenerator.generateInvoiceItemData(startDate, endDate, targetDate, billingCycleDay, getBillingPeriod(), getBillingMode());
BigDecimal numberOfBillingCycles = ZERO;
for (final RecurringInvoiceItemData item : items.getItemData()) {
@@ -84,7 +77,7 @@ public abstract class ProRationTestBase extends InvoiceTestSuiteNoDB {
}
protected BigDecimal calculateNumberOfBillingCycles(final LocalDate startDate, final LocalDate targetDate, final int billingCycleDay) throws InvalidDateSequenceException {
- final RecurringInvoiceItemDataWithNextBillingCycleDate items = getBillingModeGenerator().generateInvoiceItemData(startDate, null, targetDate, billingCycleDay, getBillingPeriod(), getBillingMode());
+ final RecurringInvoiceItemDataWithNextBillingCycleDate items = fixedAndRecurringInvoiceItemGenerator.generateInvoiceItemData(startDate, null, targetDate, billingCycleDay, getBillingPeriod(), getBillingMode());
BigDecimal numberOfBillingCycles = ZERO;
for (final RecurringInvoiceItemData item : items.getItemData()) {