use of org.killbill.billing.invoice.generator.InvoiceWithMetadata.TrackingRecordId in project killbill by killbill.
the class InvoiceDispatcher method processAccountWithLockAndInputTargetDate.
private InvoiceWithFutureNotifications processAccountWithLockAndInputTargetDate(final UUID accountId, final LocalDate originalTargetDate, final BillingEventSet billingEvents, final AccountInvoices accountInvoices, @Nullable final DryRunInfo dryRunInfo, final boolean isRescheduled, final LinkedList<PluginProperty> pluginProperties, final Map<InvoiceTiming, Long> invoiceTimings, final InternalCallContext internalCallContext) throws InvoiceApiException {
final boolean isDryRun = dryRunInfo != null;
final CallContext callContext = buildCallContext(internalCallContext);
final ImmutableAccountData account;
try {
account = accountApi.getImmutableAccountDataById(accountId, internalCallContext);
} catch (final AccountApiException e) {
log.error("Unable to generate invoice for accountId='{}', a future notification has NOT been recorded", accountId, e);
long startNano = System.nanoTime();
invoicePluginDispatcher.onFailureCall(originalTargetDate, null, accountInvoices.getInvoices(), isDryRun, isRescheduled, callContext, pluginProperties, internalCallContext);
invoiceTimings.put(InvoiceTiming.PLUGINS_COMPLETION_CALL, System.nanoTime() - startNano);
return null;
}
long startNano = System.nanoTime();
final DateTime rescheduleDate = invoicePluginDispatcher.priorCall(originalTargetDate, accountInvoices.getInvoices(), isDryRun, isRescheduled, callContext, pluginProperties, internalCallContext);
invoiceTimings.put(InvoiceTiming.PLUGINS_PRIOR_CALL, System.nanoTime() - startNano);
if (rescheduleDate != null) {
if (isDryRun) {
log.warn("Ignoring rescheduleDate='{}', delayed scheduling is unsupported in dry-run", rescheduleDate);
} else {
final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(rescheduleDate, billingEvents, internalCallContext);
setFutureNotifications(account, futureAccountNotifications, internalCallContext);
}
return null;
}
startNano = System.nanoTime();
final InvoiceWithMetadata invoiceWithMetadata = generateKillBillInvoice(account, originalTargetDate, billingEvents, accountInvoices, dryRunInfo, internalCallContext);
invoiceTimings.put(InvoiceTiming.INVOICE_GENERATION, System.nanoTime() - startNano);
final DefaultInvoice invoice = invoiceWithMetadata.getInvoice();
// Compute future notifications
final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceWithMetadata, billingEvents, internalCallContext);
// If invoice comes back null, there is nothing new to generate, we can bail early
if (invoice == null) {
startNano = System.nanoTime();
invoicePluginDispatcher.onSuccessCall(originalTargetDate, null, accountInvoices.getInvoices(), isDryRun, isRescheduled, callContext, pluginProperties, internalCallContext);
invoiceTimings.put(InvoiceTiming.PLUGINS_COMPLETION_CALL, System.nanoTime() - startNano);
if (isDryRun) {
log.info("Generated null dryRun invoice for accountId='{}', targetDate='{}'", accountId, originalTargetDate);
} else {
log.info("Generated null invoice for accountId='{}', targetDate='{}'", accountId, originalTargetDate);
final BusInternalEvent event = new DefaultNullInvoiceEvent(accountId, clock.getUTCToday(), internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId(), internalCallContext.getUserToken());
// Although we have a null invoice, it could be as a result of removing $0 USAGE (config#isUsageZeroAmountDisabled)
// and so we may still need to set the CTD for such subscriptions.
startNano = System.nanoTime();
setChargedThroughDatesNoExceptions(invoiceWithMetadata.getChargeThroughDates(), internalCallContext);
invoiceTimings.put(InvoiceTiming.SET_CHARGE_THROUGH_DT, System.nanoTime() - startNano);
setFutureNotifications(account, futureAccountNotifications, internalCallContext);
postEvent(event);
}
return null;
}
final LocalDate actualTargetDate = invoice.getTargetDate();
boolean success = false;
try {
// Generate missing credit (> 0 for generation and < 0 for use) prior we call the plugin(s)
final InvoiceItem cbaItemPreInvoicePlugins = computeCBAOnExistingInvoice(invoice, internalCallContext);
if (cbaItemPreInvoicePlugins != null) {
invoice.addInvoiceItem(cbaItemPreInvoicePlugins);
}
//
// Ask external invoice plugins if additional items (tax, etc) shall be added to the invoice
//
startNano = System.nanoTime();
final boolean invoiceUpdated = invoicePluginDispatcher.updateOriginalInvoiceWithPluginInvoiceItems(invoice, isDryRun, callContext, pluginProperties, internalCallContext);
invoiceTimings.put(InvoiceTiming.PLUGINS_ADDITIONAL_ITEMS, System.nanoTime() - startNano);
if (invoiceUpdated) {
// Remove the temporary CBA item as we need to re-compute CBA
if (cbaItemPreInvoicePlugins != null) {
invoice.removeInvoiceItem(cbaItemPreInvoicePlugins);
}
// Use credit after we call the plugin (https://github.com/killbill/killbill/issues/637)
final InvoiceItem cbaItemPostInvoicePlugins = computeCBAOnExistingInvoice(invoice, internalCallContext);
if (cbaItemPostInvoicePlugins != null) {
invoice.addInvoiceItem(cbaItemPostInvoicePlugins);
}
}
if (!isDryRun) {
// Compute whether this is a new invoice object (or just some adjustments on an existing invoice), and extract invoiceIds for later use
final Set<UUID> uniqueInvoiceIds = getUniqueInvoiceIds(invoice);
final boolean isRealInvoiceWithItems = uniqueInvoiceIds.remove(invoice.getId());
final Set<UUID> adjustedUniqueOtherInvoiceId = uniqueInvoiceIds;
logInvoiceWithItems(account, invoice, actualTargetDate, adjustedUniqueOtherInvoiceId, isRealInvoiceWithItems);
// Transformation to Invoice -> InvoiceModelDao
final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoice);
final List<InvoiceItemModelDao> invoiceItemModelDaos = transformToInvoiceModelDao(invoice.getInvoiceItems());
invoiceModelDao.addInvoiceItems(invoiceItemModelDaos);
final Set<InvoiceTrackingModelDao> trackingIds = new HashSet<>();
for (final TrackingRecordId cur : invoiceWithMetadata.getTrackingIds()) {
trackingIds.add(new InvoiceTrackingModelDao(cur.getTrackingId(), cur.getInvoiceId(), cur.getSubscriptionId(), cur.getUnitType(), cur.getRecordDate()));
}
// Commit invoice on disk
final ExistingInvoiceMetadata existingInvoiceMetadata = new ExistingInvoiceMetadata(accountInvoices.getInvoices());
startNano = System.nanoTime();
commitInvoiceAndSetFutureNotifications(account, invoiceModelDao, billingEvents, trackingIds, futureAccountNotifications, existingInvoiceMetadata, internalCallContext);
invoiceTimings.put(InvoiceTiming.COMMIT_INVOICE, System.nanoTime() - startNano);
startNano = System.nanoTime();
setChargedThroughDatesNoExceptions(invoiceWithMetadata.getChargeThroughDates(), internalCallContext);
invoiceTimings.put(InvoiceTiming.SET_CHARGE_THROUGH_DT, System.nanoTime() - startNano);
success = true;
}
} finally {
// Make sure we always set future notifications in case of errors
if (!isDryRun && !success) {
setFutureNotifications(account, futureAccountNotifications, internalCallContext);
}
if (isDryRun || success) {
final DefaultInvoice refreshedInvoice = isDryRun ? invoice : new DefaultInvoice(invoiceDao.getById(invoice.getId(), internalCallContext));
startNano = System.nanoTime();
invoicePluginDispatcher.onSuccessCall(actualTargetDate, refreshedInvoice, accountInvoices.getInvoices(), isDryRun, isRescheduled, callContext, pluginProperties, internalCallContext);
invoiceTimings.put(InvoiceTiming.PLUGINS_COMPLETION_CALL, System.nanoTime() - startNano);
} else {
startNano = System.nanoTime();
invoicePluginDispatcher.onFailureCall(actualTargetDate, invoice, accountInvoices.getInvoices(), isDryRun, isRescheduled, callContext, pluginProperties, internalCallContext);
invoiceTimings.put(InvoiceTiming.PLUGINS_COMPLETION_CALL, System.nanoTime() - startNano);
}
}
return new InvoiceWithFutureNotifications(invoice, futureAccountNotifications);
}
use of org.killbill.billing.invoice.generator.InvoiceWithMetadata.TrackingRecordId in project killbill by killbill.
the class TestUsageInArrearBase method checkTrackingIds.
//
// Each input `RawUsage` should end up creating one TrackingRecordId
// Regardless of how test records trackingId -- grouped in one, or multiple calls-- and regardless of test matrix, the logics below should remain true.
//
protected void checkTrackingIds(final List<RawUsageRecord> rawUsages, final Set<TrackingRecordId> trackingRecords) {
// Verify we have same input and output
assertEquals(rawUsages.size(), trackingRecords.size());
final Map<String, List<RawUsageRecord>> trackingIdMapping = new HashMap<>();
for (final RawUsageRecord u : rawUsages) {
if (!trackingIdMapping.containsKey(u.getTrackingId())) {
trackingIdMapping.put(u.getTrackingId(), new ArrayList<>());
}
trackingIdMapping.get(u.getTrackingId()).add(u);
}
final Set<String> trackingIds = ImmutableSet.copyOf(Iterables.transform(trackingRecords, new Function<TrackingRecordId, String>() {
@Override
public String apply(final TrackingRecordId input) {
return input.getTrackingId();
}
}));
// Verify the per trackingId input matches the per trackingId output
assertEquals(trackingIdMapping.size(), trackingIds.size());
for (final String id : trackingIdMapping.keySet()) {
final List<RawUsageRecord> rawUsageForId = trackingIdMapping.get(id);
for (RawUsageRecord u : rawUsageForId) {
final TrackingRecordId found = Iterables.tryFind(trackingRecords, new Predicate<TrackingRecordId>() {
@Override
public boolean apply(final TrackingRecordId input) {
return input.getTrackingId().equals(u.getTrackingId()) && input.getRecordDate().equals(u.getDate()) && input.getUnitType().equals(u.getUnitType());
}
}).orNull();
assertNotNull(found, "Cannot find tracking Id " + u.getTrackingId());
assertEquals(found.getSubscriptionId(), subscriptionId);
assertEquals(found.getInvoiceId(), invoiceId);
assertEquals(found.getRecordDate(), u.getDate());
assertEquals(found.getUnitType(), u.getUnitType());
}
}
}
use of org.killbill.billing.invoice.generator.InvoiceWithMetadata.TrackingRecordId in project killbill by killbill.
the class RawUsageOptimizer method getInArrearUsage.
public RawUsageOptimizerResult getInArrearUsage(final LocalDate firstEventStartDate, final LocalDate targetDate, final Iterable<InvoiceItem> existingUsageItems, final Map<String, Usage> knownUsage, @Nullable final DryRunInfo dryRunInfo, final InternalCallContext internalCallContext) {
final int configRawUsagePreviousPeriod = config.getMaxRawUsagePreviousPeriod(internalCallContext);
final LocalDate optimizedStartDate = configRawUsagePreviousPeriod >= 0 ? getOptimizedRawUsageStartDate(firstEventStartDate, targetDate, existingUsageItems, knownUsage, internalCallContext) : firstEventStartDate;
log.debug("RawUsageOptimizerResult accountRecordId='{}', configRawUsagePreviousPeriod='{}', firstEventStartDate='{}', optimizedStartDate='{}', targetDate='{}'", internalCallContext.getAccountRecordId(), configRawUsagePreviousPeriod, firstEventStartDate, optimizedStartDate, targetDate);
final List<RawUsageRecord> rawUsageData = usageApi.getRawUsageForAccount(optimizedStartDate, targetDate, dryRunInfo, internalCallContext);
final List<InvoiceTrackingModelDao> trackingIds = invoiceDao.getTrackingsByDateRange(optimizedStartDate, targetDate, internalCallContext);
final Set<TrackingRecordId> existingTrackingIds = new HashSet<TrackingRecordId>();
for (final InvoiceTrackingModelDao invoiceTrackingModelDao : trackingIds) {
existingTrackingIds.add(new TrackingRecordId(invoiceTrackingModelDao.getTrackingId(), invoiceTrackingModelDao.getInvoiceId(), invoiceTrackingModelDao.getSubscriptionId(), invoiceTrackingModelDao.getUnitType(), invoiceTrackingModelDao.getRecordDate()));
}
return new RawUsageOptimizerResult(optimizedStartDate, rawUsageData, existingTrackingIds);
}
use of org.killbill.billing.invoice.generator.InvoiceWithMetadata.TrackingRecordId in project killbill by killbill.
the class ContiguousIntervalUsageInArrear method computeMissingItemsAndNextNotificationDate.
/**
* Compute the missing usage invoice items based on what should be billed and what has been billed ($ amount comparison).
*
* @param existingUsage existing on disk usage items for the subscription
* @throws CatalogApiException
*/
public UsageInArrearItemsAndNextNotificationDate computeMissingItemsAndNextNotificationDate(final List<InvoiceItem> existingUsage) throws CatalogApiException, InvoiceApiException {
Preconditions.checkState(isBuilt.get());
if (transitionTimes.size() < 2) {
return new UsageInArrearItemsAndNextNotificationDate(ImmutableList.<InvoiceItem>of(), ImmutableSet.of(), computeNextNotificationDate());
}
final List<InvoiceItem> result = Lists.newLinkedList();
final RolledUpUnitsWithTracking allUsageWithTracking = getRolledUpUsage();
final List<RolledUpUsageWithMetadata> allUsage = allUsageWithTracking.getUsage();
final Set<TrackingRecordId> allTrackingIds = allUsageWithTracking.getTrackingIds();
final Set<TrackingRecordId> existingTrackingIds = extractTrackingIds(allExistingTrackingIds);
final Set<TrackingRecordId> newTrackingIds = Sets.filter(allTrackingIds, new Predicate<TrackingRecordId>() {
@Override
public boolean apply(final TrackingRecordId allRecord) {
return !Iterables.any(existingTrackingIds, new Predicate<TrackingRecordId>() {
@Override
public boolean apply(final TrackingRecordId existingRecord) {
return existingRecord.isSimilarRecord(allRecord);
}
});
}
});
// Each RolledUpUsage 'ru' is for a specific time period and across all units
for (final RolledUpUsageWithMetadata ru : allUsage) {
final InvoiceItem existingOverlappingItem = isContainedIntoExistingUsage(ru.getStart(), ru.getEnd(), existingUsage);
if (existingOverlappingItem != null) {
// In case of blocking situations, when re-running the invoicing code, already billed usage maybe have another start and end date
// because of blocking events. We need to skip these to avoid double billing (see gotchas in testWithPartialBlockBilling).
log.warn("Ignoring usage {} between start={} and end={} as it has already been invoiced by invoiceItemId={}", usage.getName(), ru.getStart(), ru.getEnd(), existingOverlappingItem.getId());
continue;
}
//
// Previously billed items:
//
// 1. Retrieves current price amount billed for that period of time (and usage section)
final Iterable<InvoiceItem> billedItems = getBilledItems(ru.getStart(), ru.getEnd(), existingUsage);
// 2. Verify whether previously built items have the item_details section
final boolean areAllBilledItemsWithDetails = areAllBilledItemsWithDetails(billedItems);
// 3. verify if we already billed that period - use to decide whether we should include $0 items when there is nothing to bill for.
final boolean isPeriodPreviouslyBilled = !Iterables.isEmpty(billedItems);
// 4. Computes total billed usage amount
final BigDecimal billedUsage = computeBilledUsage(billedItems);
final List<RolledUpUnit> rolledUpUnits = ru.getRolledUpUnits();
final UsageInArrearAggregate toBeBilledUsageDetails = getToBeBilledUsageDetails(ru.getStart(), ru.getEnd(), rolledUpUnits, billedItems, areAllBilledItemsWithDetails);
final BigDecimal toBeBilledUsageUnrounded = toBeBilledUsageDetails.getAmount();
// See https://github.com/killbill/killbill/issues/1124
final BigDecimal toBeBilledUsage = KillBillMoney.of(toBeBilledUsageUnrounded, getCurrency());
populateResults(ru.getStart(), ru.getEnd(), ru.getCatalogEffectiveDate(), billedUsage, toBeBilledUsage, toBeBilledUsageDetails, areAllBilledItemsWithDetails, isPeriodPreviouslyBilled, result);
}
final LocalDate nextNotificationDate = computeNextNotificationDate();
return new UsageInArrearItemsAndNextNotificationDate(result, newTrackingIds, nextNotificationDate);
}
use of org.killbill.billing.invoice.generator.InvoiceWithMetadata.TrackingRecordId in project killbill by killbill.
the class UsageInvoiceItemGenerator method generateItems.
@Override
public InvoiceGeneratorResult generateItems(final ImmutableAccountData account, final UUID invoiceId, final BillingEventSet eventSet, final AccountInvoices existingInvoices, final LocalDate targetDate, final Currency targetCurrency, final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates, final DryRunInfo dryRunInfo, final InternalCallContext internalCallContext) throws InvoiceApiException {
final Map<UUID, List<InvoiceItem>> perSubscriptionInArrearUsageItems = extractPerSubscriptionExistingInArrearUsageItems(eventSet.getUsages(), existingInvoices.getInvoices());
try {
// Pretty-print the generated invoice items from the junction events
final InvoiceItemGeneratorLogger invoiceItemGeneratorLogger = new InvoiceItemGeneratorLogger(invoiceId, account.getId(), "usage", log);
final UsageDetailMode usageDetailMode = invoiceConfig.getItemResultBehaviorMode(internalCallContext);
final LocalDate minBillingEventDate = getMinBillingEventDate(eventSet, internalCallContext);
final Set<TrackingRecordId> trackingIds = new HashSet<>();
final List<InvoiceItem> items = Lists.newArrayList();
final Iterator<BillingEvent> events = eventSet.iterator();
final boolean isDryRun = dryRunInfo != null;
RawUsageOptimizerResult rawUsgRes = 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 = internalCallContext.toLocalDate(event.getEffectiveDate());
if (eventLocalEffectiveDate.isAfter(targetDate)) {
continue;
}
// Optimize to do the usage query only once after we know there are indeed some usage items
if (rawUsgRes == null && Iterables.any(event.getUsages(), new Predicate<Usage>() {
@Override
public boolean apply(final Usage input) {
return input.getBillingMode() == BillingMode.IN_ARREAR;
}
})) {
rawUsgRes = rawUsageOptimizer.getInArrearUsage(minBillingEventDate, targetDate, Iterables.concat(perSubscriptionInArrearUsageItems.values()), eventSet.getUsages(), dryRunInfo, internalCallContext);
// Ask Kill Bill team for an optimal configuration based on your use case ;-)
if (existingInvoices.getCutoffDate() != null && existingInvoices.getCutoffDate().compareTo(rawUsgRes.getRawUsageStartDate()) > 0) {
log.warn("Detected an invoice cuttOff date={}, and usage optimized start date= {} that could lead to some issues", existingInvoices.getCutoffDate(), rawUsgRes.getRawUsageStartDate());
}
}
// None of the billing events report any usage IN_ARREAR sections
if (rawUsgRes == null) {
continue;
}
final UUID subscriptionId = event.getSubscriptionId();
if (curSubscriptionId != null && !curSubscriptionId.equals(subscriptionId)) {
final SubscriptionUsageInArrear subscriptionUsageInArrear = new SubscriptionUsageInArrear(account.getId(), invoiceId, curEvents, rawUsgRes.getRawUsage(), rawUsgRes.getExistingTrackingIds(), targetDate, rawUsgRes.getRawUsageStartDate(), usageDetailMode, invoiceConfig, internalCallContext);
final List<InvoiceItem> usageInArrearItems = perSubscriptionInArrearUsageItems.get(curSubscriptionId);
final SubscriptionUsageInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionUsageInArrear.computeMissingUsageInvoiceItems(usageInArrearItems != null ? usageInArrearItems : ImmutableList.<InvoiceItem>of(), invoiceItemGeneratorLogger, isDryRun);
final List<InvoiceItem> newInArrearUsageItems = subscriptionResult.getInvoiceItems();
items.addAll(newInArrearUsageItems);
trackingIds.addAll(subscriptionResult.getTrackingIds());
updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, subscriptionResult.getPerUsageNotificationDates(), BillingMode.IN_ARREAR, perSubscriptionFutureNotificationDates);
curEvents = Lists.newArrayList();
}
curSubscriptionId = subscriptionId;
curEvents.add(event);
}
if (curSubscriptionId != null) {
final SubscriptionUsageInArrear subscriptionUsageInArrear = new SubscriptionUsageInArrear(account.getId(), invoiceId, curEvents, rawUsgRes.getRawUsage(), rawUsgRes.getExistingTrackingIds(), targetDate, rawUsgRes.getRawUsageStartDate(), usageDetailMode, invoiceConfig, internalCallContext);
final List<InvoiceItem> usageInArrearItems = perSubscriptionInArrearUsageItems.get(curSubscriptionId);
final SubscriptionUsageInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionUsageInArrear.computeMissingUsageInvoiceItems(usageInArrearItems != null ? usageInArrearItems : ImmutableList.<InvoiceItem>of(), invoiceItemGeneratorLogger, isDryRun);
final List<InvoiceItem> newInArrearUsageItems = subscriptionResult.getInvoiceItems();
items.addAll(newInArrearUsageItems);
trackingIds.addAll(subscriptionResult.getTrackingIds());
updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, subscriptionResult.getPerUsageNotificationDates(), BillingMode.IN_ARREAR, perSubscriptionFutureNotificationDates);
}
invoiceItemGeneratorLogger.logItems();
return new InvoiceGeneratorResult(items, trackingIds);
} catch (final CatalogApiException e) {
throw new InvoiceApiException(e);
}
}
Aggregations