killbill-memoizeit
Changes
.circleci/config.yml 30(+1 -29)
invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java 35(+22 -13)
invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java 28(+17 -11)
invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalCapacityUsageInArrear.java 134(+134 -0)
invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableUsageInArrear.java 248(+248 -0)
invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java 420(+71 -349)
invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageCapacityInArrearDetail.java 49(+49 -0)
invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageConsumableInArrearDetail.java 57(+57 -0)
invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageConsumableInArrearTierUnitDetail.java 73(+73 -0)
invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageInArrearDetail.java 25(+25 -0)
invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageInArrearTierUnitDetail.java 58(+58 -0)
invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalCapacityInArrear.java 236(+149 -87)
invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java 223(+157 -66)
NEWS 8(+7 -1)
overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java 19(+14 -5)
overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java 1(+1 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java 3(+2 -1)
subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg 13(+4 -9)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java 4(+3 -1)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java 8(+5 -3)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java 4(+3 -1)
util/src/main/resources/trimTenant.sql 75(+75 -0)
Details
.circleci/config.yml 30(+1 -29)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index ca7b9fc..ccde348 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -174,46 +174,18 @@ workflows:
version: 2
build-and-test:
jobs:
- - build:
- filters:
- branches:
- only:
- - master
- - work-for-release-0.19.x
- - circle-ci-experiment
+ - build
- test-h2:
requires:
- build
- filters:
- branches:
- only:
- - master
- - circle-ci-experiment
- test-mysql:
requires:
- build
- filters:
- branches:
- only:
- - master
- - work-for-release-0.19.x
- - circle-ci-experiment
- test-postgresql:
requires:
- build
- filters:
- branches:
- only:
- - master
- - circle-ci-experiment
- integration-tests:
requires:
- test-h2
- test-mysql
- test-postgresql
- filters:
- branches:
- only:
- - master
- - work-for-release-0.19.x
- - circle-ci-experiment
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index 808e0c5..c812bfe 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -352,10 +352,11 @@ public class InvoiceDispatcher {
}
}
} else /* Dry run use cases */ {
-
final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
- final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(context.getAccountRecordId(), context.getTenantRecordId());
+ final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotificationsIterable = notificationQueue.getFutureNotificationForSearchKeys(context.getAccountRecordId(), context.getTenantRecordId());
+ // Copy the results as retrieving the iterator will issue a query each time. This also makes sure the underlying JDBC connection is closed.
+ final List<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = ImmutableList.<NotificationEventWithMetadata<NextBillingDateNotificationKey>>copyOf(futureNotificationsIterable);
final Map<UUID, DateTime> nextScheduledSubscriptionsEventMap = getNextTransitionsForSubscriptions(billingEvents);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
index fb269c5..6cfcb83 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -19,6 +19,7 @@
package org.killbill.billing.invoice.notification;
import java.io.IOException;
+import java.util.Iterator;
import java.util.UUID;
import org.joda.time.DateTime;
@@ -84,19 +85,27 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = nextBillingQueue.getFutureNotificationFromTransactionForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId(), entitySqlDaoWrapperFactory.getHandle().getConnection());
NotificationEventWithMetadata<NextBillingDateNotificationKey> existingNotificationForEffectiveDate = null;
- for (final NotificationEventWithMetadata<NextBillingDateNotificationKey> input : futureNotifications) {
- final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
- input.getEvent().isDryRunForInvoiceNotification() : false;
-
- final LocalDate notificationEffectiveLocaleDate = internalCallContext.toLocalDate(futureNotificationTime);
- final LocalDate eventEffectiveLocaleDate = internalCallContext.toLocalDate(input.getEffectiveDate());
-
- if (notificationEffectiveLocaleDate.compareTo(eventEffectiveLocaleDate) == 0 &&
- ((isDryRunForInvoiceNotification && isEventDryRunForNotifications) ||
- (!isDryRunForInvoiceNotification && !isEventDryRunForNotifications))) {
- existingNotificationForEffectiveDate = input;
+ final Iterator<NotificationEventWithMetadata<NextBillingDateNotificationKey>> iterator = futureNotifications.iterator();
+ try {
+ while (iterator.hasNext()) {
+ final NotificationEventWithMetadata<NextBillingDateNotificationKey> input = iterator.next();
+ final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
+ input.getEvent().isDryRunForInvoiceNotification() : false;
+
+ final LocalDate notificationEffectiveLocaleDate = internalCallContext.toLocalDate(futureNotificationTime);
+ final LocalDate eventEffectiveLocaleDate = internalCallContext.toLocalDate(input.getEffectiveDate());
+
+ if (notificationEffectiveLocaleDate.compareTo(eventEffectiveLocaleDate) == 0 &&
+ ((isDryRunForInvoiceNotification && isEventDryRunForNotifications) ||
+ (!isDryRunForInvoiceNotification && !isEventDryRunForNotifications))) {
+ existingNotificationForEffectiveDate = input;
+ }
}
+ } finally {
// Go through all results to close the connection
+ while (iterator.hasNext()) {
+ iterator.next();
+ }
}
if (existingNotificationForEffectiveDate == null) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java
index 4a5d7e9..b8c7d76 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -18,6 +18,7 @@
package org.killbill.billing.invoice.notification;
import java.io.IOException;
+import java.util.Iterator;
import java.util.UUID;
import org.joda.time.DateTime;
@@ -58,17 +59,22 @@ public class ParentInvoiceCommitmentPoster {
final Iterable<NotificationEventWithMetadata<ParentInvoiceCommitmentNotificationKey>> futureNotifications = commitInvoiceQueue.getFutureNotificationFromTransactionForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId(), entitySqlDaoWrapperFactory.getHandle().getConnection());
boolean existingFutureNotificationWithSameDateAndInvoiceId = false;
- for (final NotificationEventWithMetadata<ParentInvoiceCommitmentNotificationKey> input : futureNotifications) {
-
-
-
- final LocalDate notificationEffectiveLocaleDate = internalCallContext.toLocalDate(futureNotificationTime);
- final LocalDate eventEffectiveLocaleDate = internalCallContext.toLocalDate(input.getEffectiveDate());
-
- if (notificationEffectiveLocaleDate.compareTo(eventEffectiveLocaleDate) == 0 && input.getEvent().getUuidKey().equals(invoiceId)) {
- existingFutureNotificationWithSameDateAndInvoiceId = true;
+ final Iterator<NotificationEventWithMetadata<ParentInvoiceCommitmentNotificationKey>> iterator = futureNotifications.iterator();
+ try {
+ while (iterator.hasNext()) {
+ final NotificationEventWithMetadata<ParentInvoiceCommitmentNotificationKey> input = iterator.next();
+ final LocalDate notificationEffectiveLocaleDate = internalCallContext.toLocalDate(futureNotificationTime);
+ final LocalDate eventEffectiveLocaleDate = internalCallContext.toLocalDate(input.getEffectiveDate());
+
+ if (notificationEffectiveLocaleDate.compareTo(eventEffectiveLocaleDate) == 0 && input.getEvent().getUuidKey().equals(invoiceId)) {
+ existingFutureNotificationWithSameDateAndInvoiceId = true;
+ }
}
+ } finally {
// Go through all results to close the connection
+ while (iterator.hasNext()) {
+ iterator.next();
+ }
}
if (!existingFutureNotificationWithSameDateAndInvoiceId) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalCapacityUsageInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalCapacityUsageInArrear.java
new file mode 100644
index 0000000..e5321fe
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalCapacityUsageInArrear.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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.usage;
+
+import java.math.BigDecimal;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Limit;
+import org.killbill.billing.catalog.api.Tier;
+import org.killbill.billing.catalog.api.Usage;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.model.UsageInvoiceItem;
+import org.killbill.billing.invoice.usage.details.UsageCapacityInArrearDetail;
+import org.killbill.billing.invoice.usage.details.UsageInArrearDetail;
+import org.killbill.billing.invoice.usage.details.UsageInArrearTierUnitDetail;
+import org.killbill.billing.usage.RawUsage;
+import org.killbill.billing.usage.api.RolledUpUnit;
+import org.killbill.billing.util.config.definition.InvoiceConfig.UsageDetailMode;
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+
+import static org.killbill.billing.invoice.usage.UsageUtils.getCapacityInArrearTier;
+
+public class ContiguousIntervalCapacityUsageInArrear extends ContiguousIntervalUsageInArrear {
+
+ private static final Joiner joiner = Joiner.on(", ");
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ public ContiguousIntervalCapacityUsageInArrear(final Usage usage,
+ final UUID accountId,
+ final UUID invoiceId,
+ final List<RawUsage> rawSubscriptionUsage,
+ final LocalDate targetDate,
+ final LocalDate rawUsageStartDate,
+ final UsageDetailMode usageDetailMode,
+ final InternalTenantContext internalTenantContext) {
+ super(usage, accountId, invoiceId, rawSubscriptionUsage, targetDate, rawUsageStartDate, usageDetailMode, internalTenantContext);
+ }
+
+
+
+ @Override
+ protected void populateResults(final LocalDate startDate, final LocalDate endDate, final Iterable<InvoiceItem> billedItems, final BigDecimal billedUsage, final BigDecimal toBeBilledUsage, final UsageInArrearDetail toBeBilledUsageDetails, final boolean areAllBilledItemsWithDetails, final List<InvoiceItem> result) {
+ // Compute final amount by subtracting amount that was already billed.
+ if (!billedItems.iterator().hasNext() || billedUsage.compareTo(toBeBilledUsage) < 0) {
+ final BigDecimal amountToBill = toBeBilledUsage.subtract(billedUsage);
+
+ if (amountToBill.compareTo(BigDecimal.ZERO) > 0) {
+ final String itemDetails = areAllBilledItemsWithDetails ? toJson(toBeBilledUsageDetails) : null;
+ final InvoiceItem item = new UsageInvoiceItem(invoiceId, accountId, getBundleId(), getSubscriptionId(), getPlanName(),
+ getPhaseName(), usage.getName(), startDate, endDate, amountToBill, null, getCurrency(), null, itemDetails);
+ result.add(item);
+ }
+ }
+
+ }
+
+
+ @Override
+ protected UsageInArrearDetail getToBeBilledUsageDetails(final List<RolledUpUnit> rolledUpUnits, final Iterable<InvoiceItem> billedItems, final boolean areAllBilledItemsWithDetails) throws CatalogApiException {
+ return computeToBeBilledCapacityInArrear(rolledUpUnits);
+ }
+
+ private Limit getTierLimit(final Tier tier, final String unitType) {
+ for (final Limit cur : tier.getLimits()) {
+ if (cur.getUnit().getName().equals(unitType)) {
+ return cur;
+ }
+ }
+ Preconditions.checkState(false, "Could not find unit type " + unitType + " in usage tier ");
+ return null;
+ }
+
+ @VisibleForTesting
+ UsageCapacityInArrearDetail computeToBeBilledCapacityInArrear(final List<RolledUpUnit> roUnits) throws CatalogApiException {
+ Preconditions.checkState(isBuilt.get());
+
+ final List<Tier> tiers = getCapacityInArrearTier(usage);
+
+ final Set<String> perUnitTypeDetailTierLevel = new HashSet<String>();
+ int tierNum = 0;
+ final List<UsageInArrearTierUnitDetail> toBeBilledDetails = Lists.newLinkedList();
+ for (final Tier cur : tiers) {
+ tierNum++;
+ boolean complies = true;
+ for (final RolledUpUnit ro : roUnits) {
+ final Limit tierLimit = getTierLimit(cur, ro.getUnitType());
+ // We ignore the min and only look at the max Limit as the tiers should be contiguous.
+ // Specifying a -1 value for last max tier will make the validation works
+ if (tierLimit.getMax() != (double) -1 && ro.getAmount().doubleValue() > tierLimit.getMax()) {
+ complies = false;
+ } else {
+ if (!perUnitTypeDetailTierLevel.contains(ro.getUnitType())) {
+ toBeBilledDetails.add(new UsageInArrearTierUnitDetail(tierNum, ro.getUnitType(), cur.getRecurringPrice().getPrice(getCurrency()), ro.getAmount().intValue()));
+ perUnitTypeDetailTierLevel.add(ro.getUnitType());
+ }
+ }
+ }
+ if (complies) {
+ return new UsageCapacityInArrearDetail(toBeBilledDetails, cur.getRecurringPrice().getPrice(getCurrency()));
+ }
+ }
+ // Probably invalid catalog config
+ joiner.join(roUnits);
+ Preconditions.checkState(false, "Could not find tier for usage " + usage.getName() + "matching with data = " + joiner.join(roUnits));
+ return null;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableUsageInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableUsageInArrear.java
new file mode 100644
index 0000000..04073ba
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableUsageInArrear.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 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.usage;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.TieredBlock;
+import org.killbill.billing.catalog.api.Usage;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.model.UsageInvoiceItem;
+import org.killbill.billing.invoice.usage.details.UsageConsumableInArrearDetail;
+import org.killbill.billing.invoice.usage.details.UsageConsumableInArrearTierUnitDetail;
+import org.killbill.billing.invoice.usage.details.UsageInArrearDetail;
+import org.killbill.billing.usage.RawUsage;
+import org.killbill.billing.usage.api.RolledUpUnit;
+import org.killbill.billing.util.config.definition.InvoiceConfig.UsageDetailMode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+import static org.killbill.billing.invoice.usage.UsageUtils.getConsumableInArrearTieredBlocks;
+
+public class ContiguousIntervalConsumableUsageInArrear extends ContiguousIntervalUsageInArrear {
+
+ private static final Logger log = LoggerFactory.getLogger(ContiguousIntervalConsumableUsageInArrear.class);
+
+ public ContiguousIntervalConsumableUsageInArrear(final Usage usage,
+ final UUID accountId,
+ final UUID invoiceId,
+ final List<RawUsage> rawSubscriptionUsage,
+ final LocalDate targetDate,
+ final LocalDate rawUsageStartDate,
+ final UsageDetailMode usageDetailMode,
+ final InternalTenantContext internalTenantContext) {
+ super(usage, accountId, invoiceId, rawSubscriptionUsage, targetDate, rawUsageStartDate, usageDetailMode, internalTenantContext);
+ }
+
+ @Override
+ protected void populateResults(final LocalDate startDate, final LocalDate endDate, final Iterable<InvoiceItem> billedItems, final BigDecimal billedUsage, final BigDecimal toBeBilledUsage, final UsageInArrearDetail toBeBilledUsageDetails, final boolean areAllBilledItemsWithDetails, final List<InvoiceItem> result) {
+ // Compute final amount by subtracting amount that was already billed.
+ if (!billedItems.iterator().hasNext() || billedUsage.compareTo(toBeBilledUsage) < 0) {
+ // In the case past invoice items showed the details (areAllBilledItemsWithDetails=true), billed usage has already been taken into account
+ // as it part of the reconciliation logic, so no need to subtract it here
+ final BigDecimal amountToBill = areAllBilledItemsWithDetails ? toBeBilledUsage : toBeBilledUsage.subtract(billedUsage);
+
+ if (amountToBill.compareTo(BigDecimal.ZERO) > 0) {
+ if (UsageDetailMode.DETAIL == usageDetailMode) {
+ for (UsageConsumableInArrearTierUnitDetail toBeBilledUsageDetail : ((UsageConsumableInArrearDetail) toBeBilledUsageDetails).getTierDetails()) {
+ final InvoiceItem item = new UsageInvoiceItem(invoiceId, accountId, getBundleId(), getSubscriptionId(), getPlanName(),
+ getPhaseName(), usage.getName(), startDate, endDate, toBeBilledUsageDetail.getAmount(), toBeBilledUsageDetail.getTierPrice(), getCurrency(), toBeBilledUsageDetail.getQuantity(), null);
+ result.add(item);
+ }
+ } else {
+ final String itemDetails = toJson(toBeBilledUsageDetails);
+ final InvoiceItem item = new UsageInvoiceItem(invoiceId, accountId, getBundleId(), getSubscriptionId(), getPlanName(),
+ getPhaseName(), usage.getName(), startDate, endDate, amountToBill, null, getCurrency(), null, itemDetails);
+ result.add(item);
+ }
+ }
+ }
+
+ }
+
+ @Override
+ protected UsageInArrearDetail getToBeBilledUsageDetails(final List<RolledUpUnit> rolledUpUnits, final Iterable<InvoiceItem> billedItems, final boolean areAllBilledItemsWithDetails) throws CatalogApiException {
+
+ final Map<String, List<UsageConsumableInArrearTierUnitDetail>> previousUnitsUsage;
+ if (areAllBilledItemsWithDetails) {
+ previousUnitsUsage = new HashMap<String, List<UsageConsumableInArrearTierUnitDetail>>();
+ for (RolledUpUnit cur : rolledUpUnits) {
+ final List<UsageConsumableInArrearTierUnitDetail> usageInArrearDetailForUnitType = getUsageConsumableInArrearDetailForUnitType(billedItems, cur.getUnitType());
+ previousUnitsUsage.put(cur.getUnitType(), usageInArrearDetailForUnitType);
+ }
+ } else {
+ previousUnitsUsage = ImmutableMap.of();
+ }
+
+ final List<UsageConsumableInArrearTierUnitDetail> usageConsumableInArrearTierUnitDetails = new ArrayList<UsageConsumableInArrearTierUnitDetail>();
+ for (final RolledUpUnit cur : rolledUpUnits) {
+ if (!unitTypes.contains(cur.getUnitType())) {
+ log.warn("ContiguousIntervalConsumableInArrear is skipping unitType " + cur.getUnitType());
+ continue;
+ }
+ final List<UsageConsumableInArrearTierUnitDetail> previousUsage = previousUnitsUsage.containsKey(cur.getUnitType()) ? previousUnitsUsage.get(cur.getUnitType()) : ImmutableList.<UsageConsumableInArrearTierUnitDetail>of();
+
+ usageConsumableInArrearTierUnitDetails.addAll(computeToBeBilledConsumableInArrear(cur, previousUsage, areAllBilledItemsWithDetails));
+ }
+ final UsageInArrearDetail toBeBilledUsageDetails = new UsageConsumableInArrearDetail(usageConsumableInArrearTierUnitDetails);
+ return toBeBilledUsageDetails;
+ }
+
+ private List<UsageConsumableInArrearTierUnitDetail> getUsageConsumableInArrearDetailForUnitType(final Iterable<InvoiceItem> billedItems, final String unitType) {
+
+ final List<UsageConsumableInArrearTierUnitDetail> result = new ArrayList<UsageConsumableInArrearTierUnitDetail>();
+ for (final InvoiceItem bi : billedItems) {
+
+ final UsageConsumableInArrearDetail usageDetail = fromJson(bi.getItemDetails());
+ for (final UsageConsumableInArrearTierUnitDetail curDetail : usageDetail.getTierDetails()) {
+ if (curDetail.getTierUnit().equals(unitType)) {
+ result.add(curDetail);
+ }
+ }
+ }
+ return result;
+ }
+
+ @VisibleForTesting
+ List<UsageConsumableInArrearTierUnitDetail> computeToBeBilledConsumableInArrear(final RolledUpUnit roUnit, final List<UsageConsumableInArrearTierUnitDetail> previousUsage, final boolean areAllBilledItemsWithDetails) throws CatalogApiException {
+
+ Preconditions.checkState(isBuilt.get());
+ final List<TieredBlock> tieredBlocks = getConsumableInArrearTieredBlocks(usage, roUnit.getUnitType());
+
+ switch (usage.getTierBlockPolicy()) {
+ case ALL_TIERS:
+ return computeToBeBilledConsumableInArrearWith_ALL_TIERS(tieredBlocks, previousUsage, roUnit.getAmount());
+ case TOP_TIER:
+ return Arrays.asList(computeToBeBilledConsumableInArrearWith_TOP_TIER(tieredBlocks, previousUsage, roUnit.getAmount(), areAllBilledItemsWithDetails));
+ default:
+ throw new IllegalStateException("Unknown TierBlockPolicy " + usage.getTierBlockPolicy());
+ }
+ }
+
+ List<UsageConsumableInArrearTierUnitDetail> computeToBeBilledConsumableInArrearWith_ALL_TIERS(final List<TieredBlock> tieredBlocks, final List<UsageConsumableInArrearTierUnitDetail> previousUsage, final Long units) throws CatalogApiException {
+
+ List<UsageConsumableInArrearTierUnitDetail> toBeBilledDetails = Lists.newLinkedList();
+ int remainingUnits = units.intValue();
+ int tierNum = 0;
+
+ final int lastPreviousUsageTier = previousUsage.size(); // we count tier from 1, 2, ...
+ final boolean hasPreviousUsage = lastPreviousUsageTier > 0;
+
+ for (final TieredBlock tieredBlock : tieredBlocks) {
+
+ tierNum++;
+ final int blockTierSize = tieredBlock.getSize().intValue();
+ final int tmp = remainingUnits / blockTierSize + (remainingUnits % blockTierSize == 0 ? 0 : 1);
+ int nbUsedTierBlocks;
+ if (tmp > tieredBlock.getMax()) {
+ nbUsedTierBlocks = tieredBlock.getMax().intValue();
+ remainingUnits -= tieredBlock.getMax() * blockTierSize;
+ } else {
+ nbUsedTierBlocks = tmp;
+ remainingUnits = 0;
+ }
+
+ if (nbUsedTierBlocks > 0) {
+ if (hasPreviousUsage) {
+ final Integer previousUsageQuantity = tierNum <= lastPreviousUsageTier ? previousUsage.get(tierNum - 1).getQuantity() : 0;
+ if (tierNum < lastPreviousUsageTier) {
+ Preconditions.checkState(nbUsedTierBlocks == previousUsageQuantity, String.format("Expected usage for tier='%d', unit='%s' to be full, instead found units='[%d/%d]'",
+ tierNum, tieredBlock.getUnit().getName(), nbUsedTierBlocks, previousUsageQuantity));
+ } else {
+ Preconditions.checkState(nbUsedTierBlocks - previousUsageQuantity >= 0, String.format("Expected usage for tier='%d', unit='%s' to contain at least as mush as current usage, instead found units='[%d/%d]",
+ tierNum, tieredBlock.getUnit().getName(), nbUsedTierBlocks, previousUsageQuantity));
+ }
+ nbUsedTierBlocks = nbUsedTierBlocks - previousUsageQuantity;
+ }
+ if (nbUsedTierBlocks > 0) {
+ toBeBilledDetails.add(new UsageConsumableInArrearTierUnitDetail(tierNum, tieredBlock.getUnit().getName(), tieredBlock.getPrice().getPrice(getCurrency()), blockTierSize, nbUsedTierBlocks));
+ }
+ }
+ }
+ return toBeBilledDetails;
+ }
+
+ UsageConsumableInArrearTierUnitDetail computeToBeBilledConsumableInArrearWith_TOP_TIER(final List<TieredBlock> tieredBlocks, final List<UsageConsumableInArrearTierUnitDetail> previousUsage, final Long units, final boolean areAllBilledItemsWithDetails) throws CatalogApiException {
+
+ int remainingUnits = units.intValue();
+
+ // By default last last tierBlock
+ TieredBlock targetBlock = tieredBlocks.get(tieredBlocks.size() - 1);
+ int targetTierNum = tieredBlocks.size();
+ int tierNum = 0;
+ // Loop through all tier block
+ for (final TieredBlock tieredBlock : tieredBlocks) {
+
+ tierNum++;
+ final int blockTierSize = tieredBlock.getSize().intValue();
+ final int tmp = remainingUnits / blockTierSize + (remainingUnits % blockTierSize == 0 ? 0 : 1);
+ if (tmp > tieredBlock.getMax()) {
+ remainingUnits -= tieredBlock.getMax() * blockTierSize;
+ } else {
+ targetBlock = tieredBlock;
+ targetTierNum = tierNum;
+ break;
+ }
+ }
+ final int lastBlockTierSize = targetBlock.getSize().intValue();
+ final int nbBlocks = units.intValue() / lastBlockTierSize + (units.intValue() % lastBlockTierSize == 0 ? 0 : 1);
+
+ return new UsageConsumableInArrearTierUnitDetail(targetTierNum, targetBlock.getUnit().getName(), targetBlock.getPrice().getPrice(getCurrency()), targetBlock.getSize().intValue(), nbBlocks);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("ContiguousIntervalConsumableUsageInArrear{");
+ sb.append("transitionTimes=").append(transitionTimes);
+ sb.append(", billingEvents=").append(billingEvents);
+ sb.append(", rawSubscriptionUsage=").append(rawSubscriptionUsage);
+ sb.append(", rawUsageStartDate=").append(rawUsageStartDate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ public static UsageConsumableInArrearDetail fromJson(String itemDetails) {
+ UsageConsumableInArrearDetail result = null;
+ if (itemDetails != null) {
+ try {
+ result = objectMapper.readValue(itemDetails, new TypeReference<UsageConsumableInArrearDetail>() {});
+ } catch (IOException e) {
+ Preconditions.checkState(false, e.getMessage());
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java
index b49d684..7dfba4f 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java
@@ -17,10 +17,8 @@
package org.killbill.billing.invoice.usage;
-import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -32,69 +30,61 @@ import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalTenantContext;
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.Limit;
-import org.killbill.billing.catalog.api.Tier;
-import org.killbill.billing.catalog.api.TieredBlock;
import org.killbill.billing.catalog.api.Usage;
import org.killbill.billing.catalog.api.UsageType;
+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.BillingIntervalDetail;
import org.killbill.billing.invoice.model.UsageInvoiceItem;
+import org.killbill.billing.invoice.usage.details.UsageInArrearDetail;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.usage.RawUsage;
import org.killbill.billing.usage.api.RolledUpUnit;
import org.killbill.billing.usage.api.RolledUpUsage;
import org.killbill.billing.util.config.definition.InvoiceConfig.UsageDetailMode;
+import org.killbill.billing.util.jackson.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
-import static org.killbill.billing.invoice.usage.UsageUtils.getCapacityInArrearTier;
import static org.killbill.billing.invoice.usage.UsageUtils.getCapacityInArrearUnitTypes;
-import static org.killbill.billing.invoice.usage.UsageUtils.getConsumableInArrearTieredBlocks;
import static org.killbill.billing.invoice.usage.UsageUtils.getConsumableInArrearUnitTypes;
-
-
/**
* There is one such class per subscriptionId, matching a given in arrear/consumable usage section and
* referenced through a contiguous list of billing events.
*/
-public class ContiguousIntervalUsageInArrear {
+public abstract class ContiguousIntervalUsageInArrear {
private static final Logger log = LoggerFactory.getLogger(ContiguousIntervalUsageInArrear.class);
- private final List<LocalDate> transitionTimes;
- private final List<BillingEvent> billingEvents;
-
- private final Usage usage;
- private final Set<String> unitTypes;
- private final List<RawUsage> rawSubscriptionUsage;
- private final LocalDate targetDate;
- private final UUID accountId;
- private final UUID invoiceId;
- private final AtomicBoolean isBuilt;
- private final LocalDate rawUsageStartDate;
- private final InternalTenantContext internalTenantContext;
- private final UsageDetailMode usageDetailMode;
- private static final ObjectMapper objectMapper = new ObjectMapper();
+ protected final List<LocalDate> transitionTimes;
+ protected final List<BillingEvent> billingEvents;
+
+ protected final Usage usage;
+ protected final Set<String> unitTypes;
+ protected final List<RawUsage> rawSubscriptionUsage;
+ protected final LocalDate targetDate;
+ protected final UUID accountId;
+ protected final UUID invoiceId;
+ protected final AtomicBoolean isBuilt;
+ protected final LocalDate rawUsageStartDate;
+ protected final InternalTenantContext internalTenantContext;
+ protected final UsageDetailMode usageDetailMode;
+ protected static final ObjectMapper objectMapper = new ObjectMapper();
public ContiguousIntervalUsageInArrear(final Usage usage,
final UUID accountId,
@@ -171,7 +161,7 @@ public class ContiguousIntervalUsageInArrear {
* @param existingUsage existing on disk usage items for the subscription
* @throws CatalogApiException
*/
- public UsageInArrearItemsAndNextNotificationDate computeMissingItemsAndNextNotificationDate(final List<InvoiceItem> existingUsage) throws CatalogApiException {
+ public UsageInArrearItemsAndNextNotificationDate computeMissingItemsAndNextNotificationDate(final List<InvoiceItem> existingUsage) throws CatalogApiException, InvoiceApiException {
Preconditions.checkState(isBuilt.get());
@@ -194,54 +184,54 @@ public class ContiguousIntervalUsageInArrear {
}
final List<RolledUpUsage> allUsage = getRolledUpUsage();
+ // Each RolledUpUsage 'ru' is for a specific time period and across all units
for (final RolledUpUsage ru : allUsage) {
- List<UsageInArrearDetail> toBeBilledUsageDetails = Lists.newLinkedList();
- BigDecimal toBeBilledUsage = BigDecimal.ZERO;
- if (usage.getUsageType() == UsageType.CAPACITY) {
- toBeBilledUsageDetails.addAll(computeToBeBilledCapacityInArrear(ru.getRolledUpUnits()));
- } else /* UsageType.CONSUMABLE */{
-
- // Compute total price amount that should be billed for that period of time (and usage section) across unitTypes.
- for (final RolledUpUnit cur : ru.getRolledUpUnits()) {
- if (!unitTypes.contains(cur.getUnitType())) {
- log.warn("ContiguousIntervalConsumableInArrear is skipping unitType " + cur.getUnitType());
- continue;
- }
-
- toBeBilledUsageDetails.addAll(computeToBeBilledConsumableInArrear(cur));
- }
-
- }
- toBeBilledUsage = toBeBilledForUnit(toBeBilledUsageDetails);
-
- // Retrieves current price amount billed for that period of time (and usage section)
+ //
+ // 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. Computes total billed usage amount
final BigDecimal billedUsage = computeBilledUsage(billedItems);
- // Compare the two and add the missing piece if required.
- if (!billedItems.iterator().hasNext() || billedUsage.compareTo(toBeBilledUsage) < 0) {
- toBeBilledUsageDetails = reconcileExistedBilledWithToBeBilled(billedItems, toBeBilledUsageDetails);
- final BigDecimal amountToBill = toBeBilledForUnit(toBeBilledUsageDetails);
-
- if (amountToBill.compareTo(BigDecimal.ZERO) > 0) {
- if (UsageDetailMode.DETAIL == usageDetailMode && usage.getUsageType() == UsageType.CONSUMABLE){
- for (UsageInArrearDetail toBeBilledUsageDetail : toBeBilledUsageDetails){
- final InvoiceItem item = new UsageInvoiceItem(invoiceId, accountId, getBundleId(), getSubscriptionId(), getPlanName(),
- getPhaseName(), usage.getName(), ru.getStart(), ru.getEnd(), toBeBilledUsageDetail.getAmount(), toBeBilledUsageDetail.getTierPrice(), getCurrency(),toBeBilledUsageDetail.getQuantity(),null);
- result.add(item);
- }
- } else {
- final InvoiceItem item = new UsageInvoiceItem(invoiceId, accountId, getBundleId(), getSubscriptionId(), getPlanName(),
- getPhaseName(), usage.getName(), ru.getStart(), ru.getEnd(), amountToBill, null, getCurrency(),null, toJson(toBeBilledUsageDetails));
- result.add(item);
- }
- }
+ final List<RolledUpUnit> rolledUpUnits = ru.getRolledUpUnits();
+
+ final UsageInArrearDetail toBeBilledUsageDetails = getToBeBilledUsageDetails(rolledUpUnits, billedItems, areAllBilledItemsWithDetails);
+ final BigDecimal toBeBilledUsage = toBeBilledUsageDetails.getAmount();
+
+ final int billedUsageDelta = billedUsage.compareTo(toBeBilledUsage);
+ // We built more in the past than what we find now, data usage vanished?
+ if (billedUsageDelta > 0) {
+ /*
+ throw new InvoiceApiException(ErrorCode.UNEXPECTED_ERROR,
+ String.format("ILLEGAL INVOICING STATE: Usage period start='%s', end='%s', previously billed amount='%.2f', new proposed amount='%.2f'",
+ ru.getStart(), ru.getEnd(), billedUsage, toBeBilledUsage));
+ */
+ // Something remains to be billed
+ } else if (billedUsageDelta < 0) {
+ populateResults(ru.getStart(), ru.getEnd(), billedItems, billedUsage, toBeBilledUsage, toBeBilledUsageDetails, areAllBilledItemsWithDetails, result);
}
}
+ final LocalDate nextNotificationDate = computeNextNotificationDate();
+ return new UsageInArrearItemsAndNextNotificationDate(result, nextNotificationDate);
+ }
+
+ protected abstract void populateResults(final LocalDate startDate, final LocalDate endDate, final Iterable<InvoiceItem> billedItems, final BigDecimal billedUsage, final BigDecimal toBeBilledUsage, final UsageInArrearDetail toBeBilledUsageDetails, final boolean areAllBilledItemsWithDetails, final List<InvoiceItem> result);
+
+
+ protected abstract UsageInArrearDetail getToBeBilledUsageDetails(final List<RolledUpUnit> rolledUpUnits, final Iterable<InvoiceItem> billedItems, final boolean areAllBilledItemsWithDetails) throws CatalogApiException;
- final LocalDate nextNotificationdate = computeNextNotificationDate();
- return new UsageInArrearItemsAndNextNotificationDate(result, nextNotificationdate);
+ private boolean areAllBilledItemsWithDetails(final Iterable<InvoiceItem> billedItems) {
+ boolean atLeastOneItemWithoutDetails = Iterables.any(billedItems, new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ return input.getItemDetails() == null || input.getItemDetails().isEmpty();
+ }
+ });
+ return !atLeastOneItemWithoutDetails;
}
private LocalDate computeNextNotificationDate() {
@@ -310,7 +300,7 @@ public class ContiguousIntervalUsageInArrear {
if (prevRawUsage != null) {
if (prevRawUsage.getDate().compareTo(prevDate) >= 0 && prevRawUsage.getDate().compareTo(curDate) < 0) {
final Long currentAmount = perRangeUnitToAmount.get(prevRawUsage.getUnitType());
- final Long updatedAmount = computeUpdatedAmount(currentAmount, prevRawUsage.getAmount());
+ final Long updatedAmount = computeUpdatedAmount(currentAmount, prevRawUsage.getAmount());
perRangeUnitToAmount.put(prevRawUsage.getUnitType(), updatedAmount);
prevRawUsage = null;
}
@@ -331,7 +321,7 @@ public class ContiguousIntervalUsageInArrear {
}
final Long currentAmount = perRangeUnitToAmount.get(curRawUsage.getUnitType());
- final Long updatedAmount = computeUpdatedAmount(currentAmount, curRawUsage.getAmount());
+ final Long updatedAmount = computeUpdatedAmount(currentAmount, curRawUsage.getAmount());
perRangeUnitToAmount.put(curRawUsage.getUnitType(), updatedAmount);
}
}
@@ -369,133 +359,6 @@ public class ContiguousIntervalUsageInArrear {
}
}
-
- private Limit getTierLimit(final Tier tier, final String unitType) {
- for (final Limit cur : tier.getLimits()) {
- if (cur.getUnit().getName().equals(unitType)) {
- return cur;
- }
- }
- Preconditions.checkState(false, "Could not find unit type " + unitType + " in usage tier ");
- return null;
- }
-
- /**
- * @param roUnits the list of rolled up units for the period
- * @return the price amount that should be billed for that period/unitType
- * @throws CatalogApiException
- */
- @VisibleForTesting
- List<UsageInArrearDetail> computeToBeBilledCapacityInArrear(final List<RolledUpUnit> roUnits) throws CatalogApiException {
- Preconditions.checkState(isBuilt.get());
-
- final List<Tier> tiers = getCapacityInArrearTier(usage);
- int tierNum = 0;
- for (final Tier cur : tiers) {
- tierNum++;
- List<UsageInArrearDetail> toBeBilledDetails = Lists.newLinkedList();
- boolean complies = true;
- for (final RolledUpUnit ro : roUnits) {
- final Limit tierLimit = getTierLimit(cur, ro.getUnitType());
- // We ignore the min and only look at the max Limit as the tiers should be contiguous.
- // Specifying a -1 value for last max tier will make the validation works
- if (tierLimit.getMax() != (double) -1 && ro.getAmount().doubleValue() > tierLimit.getMax()) {
- complies = false;
- break;
- }
- toBeBilledDetails.add(new UsageInArrearDetail(tierNum, ro.getUnitType(), cur.getRecurringPrice().getPrice(getCurrency()), ro.getAmount().intValue(), BigDecimal.ZERO, BigDecimal.ZERO, ""));
-
- }
- if (complies) {
- toBeBilledDetails.get(toBeBilledDetails.size() - 1).setAmount(cur.getRecurringPrice().getPrice(getCurrency()));
- return toBeBilledDetails;
- }
- }
- // Probably invalid catalog config
- final Joiner joiner = Joiner.on(", ");
- joiner.join(roUnits);
- Preconditions.checkState(false, "Could not find tier for usage " + usage.getName()+ "matching with data = " + joiner.join(roUnits));
- return null;
- }
-
- /**
- * @param roUnit the rolled up unit for the period
- * @return the price amount that should be billed for that period/unitType
- * @throws CatalogApiException
- */
- @VisibleForTesting
- List<UsageInArrearDetail> computeToBeBilledConsumableInArrear(final RolledUpUnit roUnit) throws CatalogApiException {
-
- Preconditions.checkState(isBuilt.get());
- final List<TieredBlock> tieredBlocks = getConsumableInArrearTieredBlocks(usage, roUnit.getUnitType());
-
- switch (usage.getTierBlockPolicy()) {
- case ALL_TIERS:
- return computeToBeBilledConsumableInArrearWith_ALL_TIERS(tieredBlocks, roUnit.getAmount());
- case TOP_TIER:
- return Arrays.asList(computeToBeBilledConsumableInArrearWith_TOP_TIER(tieredBlocks, roUnit.getAmount()));
- default:
- throw new IllegalStateException("Unknown TierBlockPolicy " + usage.getTierBlockPolicy());
- }
- }
-
-
- List<UsageInArrearDetail> computeToBeBilledConsumableInArrearWith_ALL_TIERS(final List<TieredBlock> tieredBlocks, final Long units) throws CatalogApiException {
-
- List<UsageInArrearDetail> toBeBilledDetails = Lists.newLinkedList();
- BigDecimal result = BigDecimal.ZERO;
- int remainingUnits = units.intValue();
- int tierNum = 0;
- for (final TieredBlock tieredBlock : tieredBlocks) {
-
- tierNum++;
- final int blockTierSize = tieredBlock.getSize().intValue();
- final int tmp = remainingUnits / blockTierSize + (remainingUnits % blockTierSize == 0 ? 0 : 1);
- final int nbUsedTierBlocks;
- if (tmp > tieredBlock.getMax()) {
- nbUsedTierBlocks = tieredBlock.getMax().intValue();
- remainingUnits -= tieredBlock.getMax() * blockTierSize;
- } else {
- nbUsedTierBlocks = tmp;
- remainingUnits = 0;
- }
-
- if (nbUsedTierBlocks > 0) {
- toBeBilledDetails.add(new UsageInArrearDetail(tierNum, tieredBlock.getUnit().getName(), tieredBlock.getPrice().getPrice(getCurrency()), nbUsedTierBlocks));
- }
- }
- return toBeBilledDetails;
- }
-
- UsageInArrearDetail computeToBeBilledConsumableInArrearWith_TOP_TIER(final List<TieredBlock> tieredBlocks, final Long units) throws CatalogApiException {
-
- int remainingUnits = units.intValue();
-
- // By default last last tierBlock
- TieredBlock targetBlock = tieredBlocks.get(tieredBlocks.size() - 1);
- int targetTierNum = tieredBlocks.size();
- int tierNum = 0;
- // Loop through all tier block
- for (final TieredBlock tieredBlock : tieredBlocks) {
-
- tierNum++;
- final int blockTierSize = tieredBlock.getSize().intValue();
- final int tmp = remainingUnits / blockTierSize + (remainingUnits % blockTierSize == 0 ? 0 : 1);
- if (tmp > tieredBlock.getMax()) {
- remainingUnits -= tieredBlock.getMax() * blockTierSize;
- } else {
- targetBlock = tieredBlock;
- targetTierNum = tierNum;
- break;
- }
- }
- final int lastBlockTierSize = targetBlock.getSize().intValue();
- final int nbBlocks = units.intValue() / lastBlockTierSize + (units.intValue() % lastBlockTierSize == 0 ? 0 : 1);
-
- return new UsageInArrearDetail(targetTierNum, targetBlock.getUnit().getName(), targetBlock.getPrice().getPrice(getCurrency()), nbBlocks);
- }
-
-
/**
* @param filteredUsageForInterval the list of invoiceItem to consider
* @return the price amount that was already billed for that period and usage section (across unitTypes)
@@ -512,10 +375,10 @@ public class ContiguousIntervalUsageInArrear {
return billedAmount;
}
- Iterable<InvoiceItem> getBilledItems(final LocalDate startDate, final LocalDate endDate, final List<InvoiceItem> existingUsage) {
+ List<InvoiceItem> getBilledItems(final LocalDate startDate, final LocalDate endDate, final List<InvoiceItem> existingUsage) {
Preconditions.checkState(isBuilt.get());
- return Iterables.filter(existingUsage, new Predicate<InvoiceItem>() {
+ final Iterable<InvoiceItem> filteredResult = Iterables.filter(existingUsage, new Predicate<InvoiceItem>() {
@Override
public boolean apply(final InvoiceItem input) {
if (input.getInvoiceItemType() != InvoiceItemType.USAGE) {
@@ -528,6 +391,7 @@ public class ContiguousIntervalUsageInArrear {
usageInput.getEndDate().compareTo(endDate) <= 0;
}
});
+ return ImmutableList.copyOf(filteredResult);
}
@VisibleForTesting
@@ -599,155 +463,13 @@ public class ContiguousIntervalUsageInArrear {
}
}
- public BigDecimal toBeBilledForUnit(List<UsageInArrearDetail> toBeBilledDetails){
- BigDecimal result = BigDecimal.ZERO;
- for (UsageInArrearDetail toBeBilled: toBeBilledDetails){
- result = result.add(toBeBilled.getAmount());
+ protected String toJson(final UsageInArrearDetail usageInArrearDetail) {
+ try {
+ return objectMapper.writeValueAsString(usageInArrearDetail);
+ } catch (JsonProcessingException e) {
+ Preconditions.checkState(false, e.getMessage());
+ return null;
}
- return result;
}
- private List<UsageInArrearDetail> reconcileExistedBilledWithToBeBilled(Iterable<InvoiceItem> billedItems, List<UsageInArrearDetail> toBeBilledUsageInArrearDetails) {
- for (final InvoiceItem bi : billedItems) {
- List<UsageInArrearDetail> billedUsageItemDetails = fromJson(bi.getItemDetails());
-
- if (billedUsageItemDetails != null && billedUsageItemDetails.size() > 0) {
-
- for (final UsageInArrearDetail toBeBilledConsumable : toBeBilledUsageInArrearDetails) {
- billedUsageItemDetails = toBeBilledConsumable.reconcile(billedUsageItemDetails);
- }
-
- if (billedUsageItemDetails != null && billedUsageItemDetails.size() > 0) {
- for (final UsageInArrearDetail billedUsage : billedUsageItemDetails) {
- toBeBilledUsageInArrearDetails.add(new UsageInArrearDetail(billedUsage.getTier(), billedUsage.getTierUnit(), billedUsage.getTierPrice(),
- billedUsage.getQuantity() * -1, billedUsage.getAmount().negate(), null, bi.getId().toString()));
- }
- }
- } else {
- toBeBilledUsageInArrearDetails.get(0).setAmount(toBeBilledUsageInArrearDetails.get(0).getAmount().subtract(bi.getAmount()));
- toBeBilledUsageInArrearDetails.get(0).setQuantity(toBeBilledUsageInArrearDetails.get(0).getQuantity() - (bi.getQuantity() == null ? 0 : bi.getQuantity()));
- toBeBilledUsageInArrearDetails.get(0).setExistingUsageAmount(toBeBilledUsageInArrearDetails.get(0).getExistingUsageAmount().add(bi.getAmount()));
- toBeBilledUsageInArrearDetails.get(0).setReference(toBeBilledUsageInArrearDetails.get(0).getReference().concat(":").concat(bi.getId().toString()));
- }
- }
-
- return toBeBilledUsageInArrearDetails;
- }
-
- private static final String toJson(List<UsageInArrearDetail> toBeBilledUsageInArrearDetails) {
- String result = null;
- if (toBeBilledUsageInArrearDetails != null && toBeBilledUsageInArrearDetails.size() > 0){
- try {
- result = objectMapper.writeValueAsString(toBeBilledUsageInArrearDetails);
- } catch (JsonProcessingException e) {
- Preconditions.checkState(false, e.getMessage());
- }
- }
- return result;
- }
-
- private static final List<UsageInArrearDetail> fromJson(String itemDetails){
- List<UsageInArrearDetail> toBeBilledUsageInArrearDetails = null;
- if (itemDetails != null){
- try {
- toBeBilledUsageInArrearDetails = objectMapper.readValue(itemDetails, new TypeReference<List<UsageInArrearDetail>>() {});
- } catch (IOException e) {
- Preconditions.checkState(false, e.getMessage());
- }
- }
-
- return toBeBilledUsageInArrearDetails;
- }
-
- public static class UsageInArrearDetail {
-
- private final int tier;
- private final String tierUnit;
- private final BigDecimal tierPrice;
- private Integer quantity;
- private String reference;
- private BigDecimal existingUsageAmount;
- private BigDecimal amount;
-
- public UsageInArrearDetail(BigDecimal tierPrice, Integer quantity, BigDecimal existingUsageAmount, String reference) {
- this(0, null, tierPrice, quantity, existingUsageAmount, BigDecimal.ZERO, reference);
- }
-
- public UsageInArrearDetail(int tier, String tierUnit, BigDecimal tierPrice, Integer quantity) {
- this(tier, tierUnit, tierPrice, quantity, tierPrice.multiply(new BigDecimal(quantity)), BigDecimal.ZERO, "");
- }
-
- @JsonCreator
- public UsageInArrearDetail(@JsonProperty("tier") int tier, @JsonProperty("tierUnit") String tierUnit,
- @JsonProperty("tierPrice") BigDecimal tierPrice, @JsonProperty("quantity") Integer quantity,
- @JsonProperty("amount") BigDecimal amount, @JsonProperty("existingUsageAmount") BigDecimal existingUsageAmount,
- @JsonProperty("reference") String reference) {
- this.tier = tier;
- this.tierUnit = tierUnit;
- this.tierPrice = tierPrice;
- this.quantity = quantity;
- this.amount = amount;
- this.existingUsageAmount = existingUsageAmount;
- this.reference = reference;
- }
-
- public int getTier() {
- return tier;
- }
-
- public String getTierUnit() {
- return tierUnit;
- }
-
- public BigDecimal getTierPrice() {
- return tierPrice;
- }
-
- public Integer getQuantity() {
- return quantity;
- }
-
- public BigDecimal getExistingUsageAmount() {
- return existingUsageAmount;
- }
-
- public String getReference() {
- return reference;
- }
-
- public void setExistingUsageAmount(BigDecimal existingUsageAmount) {
- this.existingUsageAmount = existingUsageAmount;
- }
-
- public BigDecimal getAmount() {
- return amount;
- }
-
- public void setQuantity(Integer quantity) {
- this.quantity = quantity;
- }
-
- public void setAmount(BigDecimal amount) {
- this.amount = amount;
- }
-
- public void setReference(String reference) {
- this.reference = reference;
- }
-
- public List<UsageInArrearDetail> reconcile(List<UsageInArrearDetail> billedUsageItemDetails) {
- List<UsageInArrearDetail> unreconciledUsage = Lists.newLinkedList();
- for (UsageInArrearDetail billedUsageDetail : billedUsageItemDetails) {
- if (tierUnit.equals(billedUsageDetail.tierUnit)) {
- existingUsageAmount = billedUsageDetail.getAmount().abs();
- quantity = quantity - billedUsageDetail.getQuantity();
- amount = amount.subtract(existingUsageAmount);
- } else {
- unreconciledUsage.add(billedUsageDetail);
- }
- }
-
- return unreconciledUsage;
- }
- }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageCapacityInArrearDetail.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageCapacityInArrearDetail.java
new file mode 100644
index 0000000..4baa4d9
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageCapacityInArrearDetail.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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.usage.details;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class UsageCapacityInArrearDetail implements UsageInArrearDetail {
+
+ private final List<UsageInArrearTierUnitDetail> tierDetails;
+ private BigDecimal amount;
+
+ @JsonCreator
+ public UsageCapacityInArrearDetail(@JsonProperty("tierDetails") List<UsageInArrearTierUnitDetail> tierDetails,
+ @JsonProperty("amount") BigDecimal amount) {
+ this.tierDetails = tierDetails;
+ this.amount = amount;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public void setAmount(BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ public List<UsageInArrearTierUnitDetail> getTierDetails() {
+ return tierDetails;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageConsumableInArrearDetail.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageConsumableInArrearDetail.java
new file mode 100644
index 0000000..8d690bb
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageConsumableInArrearDetail.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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.usage.details;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class UsageConsumableInArrearDetail implements UsageInArrearDetail {
+
+ private final List<UsageConsumableInArrearTierUnitDetail> tierDetails;
+ private BigDecimal amount;
+
+ public UsageConsumableInArrearDetail(@JsonProperty("tierDetails") List<UsageConsumableInArrearTierUnitDetail> tierDetails) {
+ this(tierDetails, computeAmount(tierDetails));
+ }
+
+ @JsonCreator
+ public UsageConsumableInArrearDetail(@JsonProperty("tierDetails") List<UsageConsumableInArrearTierUnitDetail> tierDetails,
+ @JsonProperty("amount") BigDecimal amount) {
+ this.tierDetails = tierDetails;
+ this.amount = amount;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public List<UsageConsumableInArrearTierUnitDetail> getTierDetails() {
+ return tierDetails;
+ }
+
+ private static BigDecimal computeAmount(final List<UsageConsumableInArrearTierUnitDetail> tierDetails) {
+ BigDecimal result = BigDecimal.ZERO;
+ for (UsageConsumableInArrearTierUnitDetail toBeBilled : tierDetails) {
+ result = result.add(toBeBilled.getAmount());
+ }
+ return result;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageConsumableInArrearTierUnitDetail.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageConsumableInArrearTierUnitDetail.java
new file mode 100644
index 0000000..de7b59b
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageConsumableInArrearTierUnitDetail.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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.usage.details;
+
+import java.math.BigDecimal;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class UsageConsumableInArrearTierUnitDetail extends UsageInArrearTierUnitDetail {
+
+ private final int tierBlockSize;
+ private BigDecimal amount;
+
+ public UsageConsumableInArrearTierUnitDetail(int tier, String tierUnit, BigDecimal tierPrice, Integer tierBlockSize, Integer quantity) {
+ this(tier, tierUnit, tierPrice, tierBlockSize, quantity, tierPrice.multiply(new BigDecimal(quantity)));
+ }
+
+ @JsonCreator
+ public UsageConsumableInArrearTierUnitDetail(@JsonProperty("tier") int tier,
+ @JsonProperty("tierUnit") String tierUnit,
+ @JsonProperty("tierPrice") BigDecimal tierPrice,
+ @JsonProperty("tierBlockSize") Integer tierBlockSize,
+ @JsonProperty("quantity") Integer quantity,
+ @JsonProperty("amount") BigDecimal amount) {
+ super(tier, tierUnit, tierPrice, quantity);
+ this.amount = amount;
+ this.tierBlockSize = tierBlockSize;
+ }
+
+ public int getTier() {
+ return tier;
+ }
+
+ public String getTierUnit() {
+ return tierUnit;
+ }
+
+ public BigDecimal getTierPrice() {
+ return tierPrice;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public void setAmount(BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ public int getTierBlockSize() {
+ return tierBlockSize;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageInArrearDetail.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageInArrearDetail.java
new file mode 100644
index 0000000..c04ed95
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageInArrearDetail.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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.usage.details;
+
+import java.math.BigDecimal;
+
+public interface UsageInArrearDetail {
+
+ BigDecimal getAmount();
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageInArrearTierUnitDetail.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageInArrearTierUnitDetail.java
new file mode 100644
index 0000000..59aebc7
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/details/UsageInArrearTierUnitDetail.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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.usage.details;
+
+import java.math.BigDecimal;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class UsageInArrearTierUnitDetail {
+
+ protected final int tier;
+ protected final String tierUnit;
+ protected final BigDecimal tierPrice;
+ protected Integer quantity;
+
+ @JsonCreator
+ public UsageInArrearTierUnitDetail(@JsonProperty("tier") int tier,
+ @JsonProperty("tierUnit") String tierUnit,
+ @JsonProperty("tierPrice") BigDecimal tierPrice,
+ @JsonProperty("quantity") Integer quantity) {
+ this.tier = tier;
+ this.tierUnit = tierUnit;
+ this.tierPrice = tierPrice;
+ this.quantity = quantity;
+ }
+
+ public int getTier() {
+ return tier;
+ }
+
+ public String getTierUnit() {
+ return tierUnit;
+ }
+
+ public BigDecimal getTierPrice() {
+ return tierPrice;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java
index b104127..93bbd42 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java
@@ -33,6 +33,7 @@ import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Usage;
import org.killbill.billing.catalog.api.UsageType;
+import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.generator.InvoiceItemGenerator.InvoiceItemGeneratorLogger;
import org.killbill.billing.invoice.usage.ContiguousIntervalUsageInArrear.UsageInArrearItemsAndNextNotificationDate;
@@ -113,7 +114,7 @@ public class SubscriptionUsageInArrear {
* @param existingUsage the existing on disk usage items.
* @throws CatalogApiException
*/
- public SubscriptionUsageInArrearItemsAndNextNotificationDate computeMissingUsageInvoiceItems(final List<InvoiceItem> existingUsage, final InvoiceItemGeneratorLogger invoiceItemGeneratorLogger) throws CatalogApiException {
+ public SubscriptionUsageInArrearItemsAndNextNotificationDate computeMissingUsageInvoiceItems(final List<InvoiceItem> existingUsage, final InvoiceItemGeneratorLogger invoiceItemGeneratorLogger) throws CatalogApiException, InvoiceApiException {
final SubscriptionUsageInArrearItemsAndNextNotificationDate result = new SubscriptionUsageInArrearItemsAndNextNotificationDate();
final List<ContiguousIntervalUsageInArrear> billingEventTransitionTimePeriods = computeInArrearUsageInterval();
for (final ContiguousIntervalUsageInArrear usageInterval : billingEventTransitionTimePeriods) {
@@ -154,7 +155,10 @@ public class SubscriptionUsageInArrear {
// Add inflight usage interval if non existent
ContiguousIntervalUsageInArrear existingInterval = inFlightInArrearUsageIntervals.get(usage.getName());
if (existingInterval == null) {
- existingInterval = new ContiguousIntervalUsageInArrear(usage, accountId, invoiceId, rawSubscriptionUsage, targetDate, rawUsageStartDate, usageDetailMode, internalTenantContext);
+ existingInterval = usage.getUsageType() == UsageType.CAPACITY ?
+ new ContiguousIntervalCapacityUsageInArrear(usage, accountId, invoiceId, rawSubscriptionUsage, targetDate, rawUsageStartDate, usageDetailMode, internalTenantContext) :
+ new ContiguousIntervalConsumableUsageInArrear(usage, accountId, invoiceId, rawSubscriptionUsage, targetDate, rawUsageStartDate, usageDetailMode, internalTenantContext);
+
inFlightInArrearUsageIntervals.put(usage.getName(), existingInterval);
}
// Add billing event for that usage interval
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/UsageUtils.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/UsageUtils.java
index 69baa60..5e0e026 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/UsageUtils.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/UsageUtils.java
@@ -16,10 +16,8 @@
package org.killbill.billing.invoice.usage;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import org.killbill.billing.catalog.api.BillingMode;
@@ -40,13 +38,19 @@ public class UsageUtils {
Preconditions.checkArgument(usage.getBillingMode() == BillingMode.IN_ARREAR && usage.getUsageType() == UsageType.CONSUMABLE);
Preconditions.checkArgument(usage.getTiers().length > 0);
+
final List<TieredBlock> result = Lists.newLinkedList();
for (Tier tier : usage.getTiers()) {
+ boolean found = false;
for (TieredBlock tierBlock : tier.getTieredBlocks()) {
if (tierBlock.getUnit().getName().equals(unitType)) {
result.add(tierBlock);
+ found = true;
+ break;
}
}
+ // We expect this method to return an ordered list of TieredBlock, each for each tier.
+ Preconditions.checkState(found, "Catalog issue in usage section '%s': Missing tierBlock definition for unit '%s'", usage.getName(), unitType);
}
return result;
}
@@ -65,7 +69,6 @@ public class UsageUtils {
return result;
}
-
public static List<Tier> getCapacityInArrearTier(final Usage usage) {
Preconditions.checkArgument(usage.getBillingMode() == BillingMode.IN_ARREAR && usage.getUsageType() == UsageType.CAPACITY);
@@ -73,7 +76,6 @@ public class UsageUtils {
return ImmutableList.copyOf(usage.getTiers());
}
-
public static Set<String> getCapacityInArrearUnitTypes(final Usage usage) {
Preconditions.checkArgument(usage.getBillingMode() == BillingMode.IN_ARREAR && usage.getUsageType() == UsageType.CAPACITY);
@@ -88,5 +90,4 @@ public class UsageUtils {
return result;
}
-
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalCapacityInArrear.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalCapacityInArrear.java
index 14cbf50..8ffd615 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalCapacityInArrear.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalCapacityInArrear.java
@@ -27,19 +27,19 @@ import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.killbill.billing.catalog.DefaultLimit;
import org.killbill.billing.catalog.DefaultTier;
-import org.killbill.billing.catalog.DefaultTieredBlock;
import org.killbill.billing.catalog.DefaultUnit;
import org.killbill.billing.catalog.DefaultUsage;
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.TierBlockPolicy;
import org.killbill.billing.catalog.api.Usage;
+import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
import org.killbill.billing.invoice.model.UsageInvoiceItem;
-import org.killbill.billing.invoice.usage.ContiguousIntervalUsageInArrear.UsageInArrearDetail;
import org.killbill.billing.invoice.usage.ContiguousIntervalUsageInArrear.UsageInArrearItemsAndNextNotificationDate;
+import org.killbill.billing.invoice.usage.details.UsageCapacityInArrearDetail;
+import org.killbill.billing.invoice.usage.details.UsageInArrearTierUnitDetail;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.usage.RawUsage;
import org.killbill.billing.usage.api.RolledUpUnit;
@@ -85,11 +85,11 @@ public class TestContiguousIntervalCapacityInArrear extends TestUsageInArrearBas
final LocalDate targetDate = startDate.plusDays(1);
- final ContiguousIntervalUsageInArrear intervalCapacityInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
- createMockBillingEvent(targetDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),
- BillingPeriod.MONTHLY,
- Collections.<Usage>emptyList())
- );
+ final ContiguousIntervalUsageInArrear intervalCapacityInArrear = createContiguousIntervalCapacityInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
+ createMockBillingEvent(targetDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),
+ BillingPeriod.MONTHLY,
+ Collections.<Usage>emptyList())
+ );
final List<InvoiceItem> existingUsage = Lists.newArrayList();
final UsageInvoiceItem ii1 = new UsageInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usage.getName(), startDate, endDate, BigDecimal.TEN, currency);
@@ -117,7 +117,6 @@ public class TestContiguousIntervalCapacityInArrear extends TestUsageInArrearBas
@Test(groups = "fast")
public void testComputeBilledUsage() throws CatalogApiException {
-
final DefaultUnit unit1 = new DefaultUnit().setName("unit1");
final DefaultUnit unit2 = new DefaultUnit().setName("unit2");
final DefaultUnit unit3 = new DefaultUnit().setName("unit3");
@@ -138,73 +137,83 @@ public class TestContiguousIntervalCapacityInArrear extends TestUsageInArrearBas
final DefaultLimit limit3_3 = new DefaultLimit().setUnit(unit3).setMax((double) -1).setMin((double) -1);
final DefaultTier tier3 = createDefaultTierWithLimits(new BigDecimal("30.0"), limit3_1, limit3_2, limit3_3);
-
final DefaultUsage usage = createCapacityInArrearUsage(usageName, BillingPeriod.MONTHLY, tier1, tier2, tier3);
final LocalDate targetDate = new LocalDate(2014, 03, 20);
- final ContiguousIntervalUsageInArrear intervalCapacityInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
- createMockBillingEvent(targetDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),
- BillingPeriod.MONTHLY,
- Collections.<Usage>emptyList())
- );
+ final ContiguousIntervalCapacityUsageInArrear intervalCapacityInArrear = createContiguousIntervalCapacityInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
+ createMockBillingEvent(targetDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),
+ BillingPeriod.MONTHLY,
+ Collections.<Usage>emptyList())
+ );
// Tier 1 (both units from tier 1)
- List<UsageInArrearDetail> result = intervalCapacityInArrear.computeToBeBilledCapacityInArrear(ImmutableList.<RolledUpUnit>of(new DefaultRolledUpUnit("unit1", 100L),
- new DefaultRolledUpUnit("unit2", 1000L),
- new DefaultRolledUpUnit("unit3", 50L)));
- assertEquals(result.size(), 3);
- assertTrue(intervalCapacityInArrear.toBeBilledForUnit(result).compareTo(BigDecimal.TEN) == 0);
+ UsageCapacityInArrearDetail result = intervalCapacityInArrear.computeToBeBilledCapacityInArrear(ImmutableList.<RolledUpUnit>of(new DefaultRolledUpUnit("unit1", 100L),
+ new DefaultRolledUpUnit("unit2", 1000L),
+ new DefaultRolledUpUnit("unit3", 50L)));
+ assertEquals(result.getTierDetails().size(), 3);
+ assertTrue(result.getAmount().compareTo(BigDecimal.TEN) == 0);
// Tier 2 (only one unit from tier 1)
result = intervalCapacityInArrear.computeToBeBilledCapacityInArrear(ImmutableList.<RolledUpUnit>of(new DefaultRolledUpUnit("unit1", 100L),
new DefaultRolledUpUnit("unit2", 1001L)));
- assertTrue(intervalCapacityInArrear.toBeBilledForUnit(result).compareTo(new BigDecimal("20.0")) == 0);
+ assertTrue(result.getAmount().compareTo(new BigDecimal("20.0")) == 0);
// Tier 2 (only one unit from tier 1)
result = intervalCapacityInArrear.computeToBeBilledCapacityInArrear(ImmutableList.<RolledUpUnit>of(new DefaultRolledUpUnit("unit1", 101L),
new DefaultRolledUpUnit("unit2", 1000L)));
- assertTrue(intervalCapacityInArrear.toBeBilledForUnit(result).compareTo(new BigDecimal("20.0")) == 0);
-
+ assertTrue(result.getAmount().compareTo(new BigDecimal("20.0")) == 0);
// Tier 2 (both units from tier 2)
result = intervalCapacityInArrear.computeToBeBilledCapacityInArrear(ImmutableList.<RolledUpUnit>of(new DefaultRolledUpUnit("unit1", 101L),
new DefaultRolledUpUnit("unit2", 1001L)));
- assertTrue(intervalCapacityInArrear.toBeBilledForUnit(result).compareTo(new BigDecimal("20.0")) == 0);
+ assertTrue(result.getAmount().compareTo(new BigDecimal("20.0")) == 0);
// Tier 3 (only one unit from tier 3)
result = intervalCapacityInArrear.computeToBeBilledCapacityInArrear(ImmutableList.<RolledUpUnit>of(new DefaultRolledUpUnit("unit1", 10L),
new DefaultRolledUpUnit("unit2", 2001L)));
- assertTrue(intervalCapacityInArrear.toBeBilledForUnit(result).compareTo(new BigDecimal("30.0")) == 0);
+ assertTrue(result.getAmount().compareTo(new BigDecimal("30.0")) == 0);
}
@Test(groups = "fast")
- public void testComputeMissingItems() throws CatalogApiException {
+ public void testComputeMissingItems() throws CatalogApiException, InvoiceApiException {
final LocalDate startDate = new LocalDate(2014, 03, 20);
final LocalDate firstBCDDate = new LocalDate(2014, 04, 15);
final LocalDate endDate = new LocalDate(2014, 05, 15);
- // 2 items for startDate - firstBCDDate
final List<RawUsage> rawUsages = new ArrayList<RawUsage>();
- rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "unit", 130L));
- rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "unit", 271L));
- // 1 items for firstBCDDate - endDate
- rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 04, 15), "unit", 199L));
+ //
+ // First period: startDate - firstBCDDate
+ //
+ // 2 items for unit1
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "unit1", 130L));
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "unit1", 271L));
+ // 1 items for unit2
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 24), "unit2", 10L));
+
+ //
+ // Second period: firstBCDDate - endDate
+ //
+ // 1 items unit1
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 04, 15), "unit1", 199L));
+ // 1 items unit2
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 04, 15), "unit2", 20L));
- final DefaultUnit unit = new DefaultUnit().setName("unit");
- final DefaultLimit limit = new DefaultLimit().setUnit(unit).setMax((double) -1);
+ final DefaultUnit unit1 = new DefaultUnit().setName("unit1");
+ final DefaultLimit limit1 = new DefaultLimit().setUnit(unit1).setMax((double) -1);
- final DefaultTier tier = createDefaultTierWithLimits(BigDecimal.TEN, limit);
+ final DefaultUnit unit2 = new DefaultUnit().setName("unit2");
+ final DefaultLimit limit2 = new DefaultLimit().setUnit(unit2).setMax((double) -1);
- final DefaultUsage usage = createCapacityInArrearUsage(usageName, BillingPeriod.MONTHLY, tier);
+ final DefaultTier tier = createDefaultTierWithLimits(BigDecimal.TEN, limit1, limit2);
+ final DefaultUsage usage = createCapacityInArrearUsage(usageName, BillingPeriod.MONTHLY, tier);
final LocalDate targetDate = endDate;
-
- final BillingEvent event1 = createMockBillingEvent(startDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),BillingPeriod.MONTHLY, Collections.<Usage>emptyList());
+ final BillingEvent event1 = createMockBillingEvent(startDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.<Usage>emptyList());
final BillingEvent event2 = createMockBillingEvent(endDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.<Usage>emptyList());
- final ContiguousIntervalUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, rawUsages, targetDate, true, event1, event2);
+ final ContiguousIntervalCapacityUsageInArrear intervalCapacityInArrear = createContiguousIntervalCapacityInArrear(usage, rawUsages, targetDate, true, event1, event2);
final List<InvoiceItem> invoiceItems = new ArrayList<InvoiceItem>();
final InvoiceItem ii1 = new UsageInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usage.getName(), startDate, firstBCDDate, BigDecimal.ONE, currency);
@@ -213,7 +222,7 @@ public class TestContiguousIntervalCapacityInArrear extends TestUsageInArrearBas
final InvoiceItem ii2 = new UsageInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usage.getName(), firstBCDDate, endDate, BigDecimal.ONE, currency);
invoiceItems.add(ii2);
- final UsageInArrearItemsAndNextNotificationDate usageResult = intervalConsumableInArrear.computeMissingItemsAndNextNotificationDate(invoiceItems);
+ final UsageInArrearItemsAndNextNotificationDate usageResult = intervalCapacityInArrear.computeMissingItemsAndNextNotificationDate(invoiceItems);
final List<InvoiceItem> rawResults = usageResult.getInvoiceItems();
assertEquals(rawResults.size(), 4);
@@ -224,7 +233,6 @@ public class TestContiguousIntervalCapacityInArrear extends TestUsageInArrearBas
}
}));
-
assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("9.0")), 0, String.format("%s != 9.0", result.get(0).getAmount()));
assertEquals(result.get(0).getCurrency(), Currency.BTC);
assertEquals(result.get(0).getAccountId(), accountId);
@@ -249,82 +257,137 @@ public class TestContiguousIntervalCapacityInArrear extends TestUsageInArrearBas
}
@Test(groups = "fast")
- public void testComputeMissingItemsAggregateMode() throws CatalogApiException, IOException {
-
- testComputeMissingItemsByMode(UsageDetailMode.AGGREGATE);
+ public void testMultipleItemsAndTiersAggregateMode() throws CatalogApiException, IOException, InvoiceApiException {
+ testMultipleItemsAndTiers(UsageDetailMode.AGGREGATE);
}
@Test(groups = "fast")
- public void testComputeMissingItemsDetailMode() throws CatalogApiException, IOException {
-
- testComputeMissingItemsByMode(UsageDetailMode.DETAIL);
+ public void testMultipleItemsAndTiersDetailMode() throws CatalogApiException, IOException, InvoiceApiException {
+ testMultipleItemsAndTiers(UsageDetailMode.DETAIL);
}
- private void testComputeMissingItemsByMode(UsageDetailMode usageDetailMode) throws CatalogApiException, IOException {
+ private void testMultipleItemsAndTiers(UsageDetailMode usageDetailMode) throws CatalogApiException, IOException, InvoiceApiException {
// Case 1
List<RawUsage> rawUsages = new ArrayList<RawUsage>();
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 99L));
- List<InvoiceItem> result = produceInvoiceItems(rawUsages, usageDetailMode);
+ List<InvoiceItem> result = produceInvoiceItems(rawUsages, usageDetailMode, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 1);
- assertEquals(result.get(0).getAmount().compareTo(BigDecimal.ONE),0, String.format("%s != 1.0", result.get(0).getAmount()));
+ assertEquals(result.get(0).getAmount().compareTo(BigDecimal.ONE), 0, String.format("%s != 1.0", result.get(0).getAmount()));
- List<UsageInArrearDetail> itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ UsageCapacityInArrearDetail itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<UsageCapacityInArrearDetail>() {});
+ assertEquals(itemDetails.getAmount().compareTo(BigDecimal.ONE), 0);
+ assertEquals(itemDetails.getTierDetails().size(), 2);
+
+ List<UsageInArrearTierUnitDetail> itemUnitDetails = itemDetails.getTierDetails();
// BAR item detail
- assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
- assertEquals(itemDetails.get(0).getTier(), 1);
- assertEquals(itemDetails.get(0).getQuantity().intValue(), 99);
- assertEquals(itemDetails.get(0).getTierPrice().compareTo(BigDecimal.ONE), 0);
+ assertEquals(itemUnitDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemUnitDetails.get(0).getTier(), 1);
+ assertEquals(itemUnitDetails.get(0).getQuantity().intValue(), 99);
+ assertEquals(itemUnitDetails.get(0).getTierPrice().compareTo(BigDecimal.ONE), 0);
// FOO item detail
- assertEquals(itemDetails.get(1).getTierUnit(), "FOO");
- assertEquals(itemDetails.get(1).getTier(), 1);
- assertEquals(itemDetails.get(1).getQuantity().intValue(), 5);
- assertEquals(itemDetails.get(1).getTierPrice().compareTo(BigDecimal.ONE), 0);
+ assertEquals(itemUnitDetails.get(1).getTierUnit(), "FOO");
+ assertEquals(itemUnitDetails.get(1).getTier(), 1);
+ assertEquals(itemUnitDetails.get(1).getQuantity().intValue(), 5);
+ assertEquals(itemUnitDetails.get(1).getTierPrice().compareTo(BigDecimal.ONE), 0);
// Case 2
rawUsages = new ArrayList<RawUsage>();
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
- result = produceInvoiceItems(rawUsages, usageDetailMode);
+ result = produceInvoiceItems(rawUsages, usageDetailMode, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 1);
- assertEquals(result.get(0).getAmount().compareTo(BigDecimal.TEN),0, String.format("%s != 10.0", result.get(0).getAmount()));
+ assertEquals(result.get(0).getAmount().compareTo(BigDecimal.TEN), 0, String.format("%s != 10.0", result.get(0).getAmount()));
+
+ itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<UsageCapacityInArrearDetail>() {});
+ assertEquals(itemDetails.getAmount().compareTo(BigDecimal.TEN), 0);
+ assertEquals(itemDetails.getTierDetails().size(), 2);
+ itemUnitDetails = itemDetails.getTierDetails();
- itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
- // BAR item detail
- assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
- assertEquals(itemDetails.get(0).getTier(), 2);
- assertEquals(itemDetails.get(0).getQuantity().intValue(), 101);
- assertEquals(itemDetails.get(0).getTierPrice().compareTo(BigDecimal.TEN), 0);
// FOO item detail
- assertEquals(itemDetails.get(1).getTierUnit(), "FOO");
- assertEquals(itemDetails.get(1).getTier(), 2);
- assertEquals(itemDetails.get(1).getQuantity().intValue(), 5);
- assertEquals(itemDetails.get(1).getTierPrice().compareTo(BigDecimal.TEN), 0);
+ assertEquals(itemUnitDetails.get(0).getTierUnit(), "FOO");
+ assertEquals(itemUnitDetails.get(0).getTier(), 1);
+ assertEquals(itemUnitDetails.get(0).getQuantity().intValue(), 5);
+ assertEquals(itemUnitDetails.get(0).getTierPrice().compareTo(BigDecimal.ONE), 0);
+
+ // BAR item detail
+ assertEquals(itemUnitDetails.get(1).getTierUnit(), "BAR");
+ assertEquals(itemUnitDetails.get(1).getTier(), 2);
+ assertEquals(itemUnitDetails.get(1).getQuantity().intValue(), 101);
+ assertEquals(itemUnitDetails.get(1).getTierPrice().compareTo(BigDecimal.TEN), 0);
// Case 3
rawUsages = new ArrayList<RawUsage>();
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 75L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
- result = produceInvoiceItems(rawUsages, usageDetailMode);
+ result = produceInvoiceItems(rawUsages, usageDetailMode, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 1);
- assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("100.0")),0, String.format("%s != 100.0", result.get(0).getAmount()));
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("100.0")), 0, String.format("%s != 100.0", result.get(0).getAmount()));
+
+ itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<UsageCapacityInArrearDetail>() {});
+ assertEquals(itemDetails.getAmount().compareTo(new BigDecimal("100.0")), 0);
+ assertEquals(itemDetails.getTierDetails().size(), 2);
+ itemUnitDetails = itemDetails.getTierDetails();
- itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
// BAR item detail
- assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
- assertEquals(itemDetails.get(0).getTier(), 3);
- assertEquals(itemDetails.get(0).getQuantity().intValue(), 101);
- assertEquals(itemDetails.get(0).getTierPrice().compareTo(new BigDecimal("100.0")), 0);
+ assertEquals(itemUnitDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemUnitDetails.get(0).getTier(), 2);
+ assertEquals(itemUnitDetails.get(0).getQuantity().intValue(), 101);
+ assertEquals(itemUnitDetails.get(0).getTierPrice().compareTo(new BigDecimal("10.0")), 0);
+
// FOO item detail
- assertEquals(itemDetails.get(1).getTierUnit(), "FOO");
- assertEquals(itemDetails.get(1).getTier(), 3);
- assertEquals(itemDetails.get(1).getQuantity().intValue(), 75);
- assertEquals(itemDetails.get(1).getTierPrice().compareTo(new BigDecimal("100.0")), 0);
+ assertEquals(itemUnitDetails.get(1).getTierUnit(), "FOO");
+ assertEquals(itemUnitDetails.get(1).getTier(), 3);
+ assertEquals(itemUnitDetails.get(1).getQuantity().intValue(), 75);
+ assertEquals(itemUnitDetails.get(1).getTierPrice().compareTo(new BigDecimal("100.0")), 0);
+
}
- private List<InvoiceItem> produceInvoiceItems(List<RawUsage> rawUsages, UsageDetailMode usageDetailMode) throws CatalogApiException {
+ @Test(groups = "fast")
+ public void testMultipleItemsAndTiersWithExistingItems() throws CatalogApiException, IOException, InvoiceApiException {
+
+ // let's assume we have some existing usage
+ final UsageInArrearTierUnitDetail existingFooUsageTier1 = new UsageInArrearTierUnitDetail(1, "FOO", BigDecimal.ONE, 9);
+ final UsageInArrearTierUnitDetail existingBarUsageTier2 = new UsageInArrearTierUnitDetail(2, "BAR", BigDecimal.TEN, 200);
+
+
+ List<RawUsage> rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 60L)); // tier 3
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 200L)); // tier 2
+
+ final List<UsageInArrearTierUnitDetail> existingUsage = ImmutableList.of(existingFooUsageTier1, existingBarUsageTier2);
+
+ final String existingUsageJson = objectMapper.writeValueAsString(new UsageCapacityInArrearDetail(existingUsage, BigDecimal.TEN));
+
+ final List<InvoiceItem> existingItems = new ArrayList<InvoiceItem>();
+ final InvoiceItem ii1 = new UsageInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usageName, new LocalDate(2014, 03, 20), new LocalDate(2014, 04, 15), BigDecimal.TEN, null, currency, null, existingUsageJson);
+ existingItems.add(ii1);
+
+ List<InvoiceItem> result = produceInvoiceItems(rawUsages, UsageDetailMode.AGGREGATE, existingItems);
+ assertEquals(result.size(), 1);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("90.00")), 0, String.format("%s != 90.0", result.get(0).getAmount()));
+
+ UsageCapacityInArrearDetail itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<UsageCapacityInArrearDetail>() {});
+ assertEquals(itemDetails.getAmount().compareTo(new BigDecimal("100.00")), 0);
+ assertEquals(itemDetails.getTierDetails().size(), 2);
+
+ List<UsageInArrearTierUnitDetail> itemUnitDetails = itemDetails.getTierDetails();
+
+ // BAR item detail
+ assertEquals(itemUnitDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemUnitDetails.get(0).getTier(), 2);
+ assertEquals(itemUnitDetails.get(0).getQuantity().intValue(), 200);
+ assertEquals(itemUnitDetails.get(0).getTierPrice().compareTo(BigDecimal.TEN), 0);
+ // FOO item detail
+ assertEquals(itemUnitDetails.get(1).getTierUnit(), "FOO");
+ assertEquals(itemUnitDetails.get(1).getTier(), 3);
+ assertEquals(itemUnitDetails.get(1).getQuantity().intValue(), 60);
+ assertEquals(itemUnitDetails.get(1).getTierPrice().compareTo(new BigDecimal("100.00")), 0);
+ }
+
+ private List<InvoiceItem> produceInvoiceItems(List<RawUsage> rawUsages, UsageDetailMode usageDetailMode, List<InvoiceItem> existingItems) throws CatalogApiException, InvoiceApiException {
final LocalDate startDate = new LocalDate(2014, 03, 20);
final LocalDate firstBCDDate = new LocalDate(2014, 04, 15);
@@ -349,12 +412,12 @@ public class TestContiguousIntervalCapacityInArrear extends TestUsageInArrearBas
final LocalDate targetDate = endDate;
- final BillingEvent event1 = createMockBillingEvent(startDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),BillingPeriod.MONTHLY, Collections.<Usage>emptyList());
+ final BillingEvent event1 = createMockBillingEvent(startDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.<Usage>emptyList());
final BillingEvent event2 = createMockBillingEvent(endDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.<Usage>emptyList());
- final ContiguousIntervalUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, rawUsages, targetDate, true, usageDetailMode, event1, event2);
+ final ContiguousIntervalCapacityUsageInArrear intervalCapacityInArrear = createContiguousIntervalCapacityInArrear(usage, rawUsages, targetDate, true, usageDetailMode, event1, event2);
- final UsageInArrearItemsAndNextNotificationDate usageResult = intervalConsumableInArrear.computeMissingItemsAndNextNotificationDate(ImmutableList.<InvoiceItem>of());
+ final UsageInArrearItemsAndNextNotificationDate usageResult = intervalCapacityInArrear.computeMissingItemsAndNextNotificationDate(existingItems);
final List<InvoiceItem> rawResults = usageResult.getInvoiceItems();
final List<InvoiceItem> result = ImmutableList.copyOf(Iterables.filter(rawResults, new Predicate<InvoiceItem>() {
@Override
@@ -363,7 +426,7 @@ public class TestContiguousIntervalCapacityInArrear extends TestUsageInArrearBas
}
}));
- for (InvoiceItem item: result) {
+ for (InvoiceItem item : result) {
assertEquals(item.getCurrency(), Currency.BTC);
assertEquals(item.getAccountId(), accountId);
assertEquals(item.getBundleId(), bundleId);
@@ -378,5 +441,4 @@ public class TestContiguousIntervalCapacityInArrear extends TestUsageInArrearBas
return result;
}
-
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java
index 3c63a05..c7ebfa4 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java
@@ -22,10 +22,7 @@ import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Map;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
@@ -37,11 +34,14 @@ import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.TierBlockPolicy;
import org.killbill.billing.catalog.api.Usage;
+import org.killbill.billing.catalog.api.UsageType;
+import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
import org.killbill.billing.invoice.model.UsageInvoiceItem;
-import org.killbill.billing.invoice.usage.ContiguousIntervalUsageInArrear.UsageInArrearDetail;
import org.killbill.billing.invoice.usage.ContiguousIntervalUsageInArrear.UsageInArrearItemsAndNextNotificationDate;
+import org.killbill.billing.invoice.usage.details.UsageConsumableInArrearDetail;
+import org.killbill.billing.invoice.usage.details.UsageConsumableInArrearTierUnitDetail;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.usage.RawUsage;
import org.killbill.billing.usage.api.RolledUpUsage;
@@ -52,10 +52,7 @@ import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
@@ -64,6 +61,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearBase {
@@ -143,13 +141,13 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
final LocalDate targetDate = new LocalDate(2014, 03, 20);
- final ContiguousIntervalUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
+ final ContiguousIntervalConsumableUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
createMockBillingEvent(targetDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),
BillingPeriod.MONTHLY,
Collections.<Usage>emptyList())
);
- List<UsageInArrearDetail> result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 111L));
+ List<UsageConsumableInArrearTierUnitDetail> result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 111L), ImmutableList.<UsageConsumableInArrearTierUnitDetail>of(), true);
assertEquals(result.size(), 3);
// 111 = 10 (tier1) + 100 (tier2) + 1 (tier3) => 10 * 1.5 + 100 * 1 + 1 * 0.5 = 115.5
assertEquals(result.get(0).getAmount(), new BigDecimal("15.0"));
@@ -171,13 +169,13 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
final LocalDate targetDate = new LocalDate(2014, 03, 20);
- final ContiguousIntervalUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
+ final ContiguousIntervalConsumableUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
createMockBillingEvent(targetDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),
BillingPeriod.MONTHLY,
Collections.<Usage>emptyList())
);
- List<UsageInArrearDetail> result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 5325L));
+ List<UsageConsumableInArrearTierUnitDetail> result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 5325L), ImmutableList.<UsageConsumableInArrearTierUnitDetail>of(), true);
assertEquals(result.size(), 2);
// 5000 = 1000 (tier1) + 4325 (tier2) => 10 + 5 = 15
@@ -203,7 +201,7 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
final LocalDate targetDate = new LocalDate(2014, 03, 20);
- final ContiguousIntervalUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
+ final ContiguousIntervalConsumableUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
createMockBillingEvent(targetDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),
BillingPeriod.MONTHLY,
Collections.<Usage>emptyList())
@@ -211,23 +209,23 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
//
// In this model unit amount is first used to figure out which tier we are in, and then we price all unit at that 'target' tier
//
- List<UsageInArrearDetail> inputTier1 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 1000L));
+ List<UsageConsumableInArrearTierUnitDetail> inputTier1 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 1000L), ImmutableList.<UsageConsumableInArrearTierUnitDetail>of(), true);
assertEquals(inputTier1.size(), 1);
// 1000 units => (tier1) : 1000 / 100 + 1000 % 100 = 10
assertEquals(inputTier1.get(0).getAmount(), new BigDecimal("10"));
- List<UsageInArrearDetail> inputTier2 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 101000L));
+ List<UsageConsumableInArrearTierUnitDetail> inputTier2 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 101000L), ImmutableList.<UsageConsumableInArrearTierUnitDetail>of(), true);
assertEquals(inputTier2.size(), 1);
// 101000 units => (tier2) : 101000 / 1000 + 101000 % 1000 = 101 + 0 = 101
assertEquals(inputTier2.get(0).getAmount(), new BigDecimal("101"));
- List<UsageInArrearDetail> inputTier3 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 101001L));
+ List<UsageConsumableInArrearTierUnitDetail> inputTier3 = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 101001L), ImmutableList.<UsageConsumableInArrearTierUnitDetail>of(), true);
assertEquals(inputTier3.size(), 1);
// 101001 units => (tier3) : 101001 / 1000 + 101001 % 1000 = 101 + 1 = 102 units => $51
assertEquals(inputTier3.get(0).getAmount(), new BigDecimal("51.0"));
// If we pass the maximum of the last tier, we price all units at the last tier
- List<UsageInArrearDetail> inputLastTier = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 300000L));
+ List<UsageConsumableInArrearTierUnitDetail> inputLastTier = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 300000L), ImmutableList.<UsageConsumableInArrearTierUnitDetail>of(), true);
assertEquals(inputLastTier.size(), 1);
// 300000 units => (tier3) : 300000 / 1000 + 300000 % 1000 = 300 units => $150
assertEquals(inputLastTier.get(0).getAmount(), new BigDecimal("150.0"));
@@ -250,13 +248,13 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
final LocalDate targetDate = new LocalDate(2014, 03, 20);
- final ContiguousIntervalUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
+ final ContiguousIntervalConsumableUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
createMockBillingEvent(targetDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),
BillingPeriod.MONTHLY,
Collections.<Usage>emptyList())
);
- List<UsageInArrearDetail> result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 111L));
+ List<UsageConsumableInArrearTierUnitDetail> result = intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("unit", 111L), ImmutableList.<UsageConsumableInArrearTierUnitDetail>of(), true);
assertEquals(result.size(), 1);
// 111 = 111 * 0.5 =
@@ -267,7 +265,7 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
@Test(groups = "fast")
- public void testComputeMissingItems() throws CatalogApiException, IOException {
+ public void testComputeMissingItems() throws CatalogApiException, IOException, InvoiceApiException {
final LocalDate startDate = new LocalDate(2014, 03, 20);
final LocalDate firstBCDDate = new LocalDate(2014, 04, 15);
@@ -321,11 +319,12 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
assertTrue(result.get(0).getStartDate().compareTo(startDate) == 0);
assertTrue(result.get(0).getEndDate().compareTo(firstBCDDate) == 0);
- // check item detail
- List<UsageInArrearDetail> itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
-
+ assertNotNull(result.get(0).getItemDetails());
+ UsageConsumableInArrearDetail usageDetail = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<UsageConsumableInArrearDetail>() {});
+ List<UsageConsumableInArrearTierUnitDetail> itemDetails = usageDetail.getTierDetails();
assertEquals(itemDetails.size(), 1);
- assertEquals(itemDetails.get(0).getAmount().compareTo(result.get(0).getAmount()),0);
+ // Because we did not have the details before, the new details don't take into account the
+ assertEquals(itemDetails.get(0).getAmount().compareTo(new BigDecimal("5.0")), 0);
// Invoiced for 1 BTC and used 199 => 2 blocks => 2 BTC so remaining piece should be 1 BTC
assertEquals(result.get(1).getAmount().compareTo(new BigDecimal("1.0")), 0, String.format("%s != 1.0", result.get(0).getAmount()));
@@ -338,11 +337,13 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
assertEquals(result.get(1).getUsageName(), usage.getName());
assertTrue(result.get(1).getStartDate().compareTo(firstBCDDate) == 0);
assertTrue(result.get(1).getEndDate().compareTo(endDate) == 0);
+ assertNotNull(result.get(1).getItemDetails());
+
+ usageDetail = objectMapper.readValue(result.get(1).getItemDetails(), new TypeReference<UsageConsumableInArrearDetail>() {});
+ itemDetails = usageDetail.getTierDetails();
+ assertEquals(itemDetails.size(), 1);
+ assertEquals(itemDetails.get(0).getAmount().compareTo(new BigDecimal("2.0")), 0);
- // check item detail
- List<UsageInArrearDetail> itemDetails2 = objectMapper.readValue(result.get(1).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
- assertEquals(itemDetails2.size(), 1);
- assertEquals(itemDetails2.get(0).getAmount().compareTo(result.get(1).getAmount()),0);
}
@Test(groups = "fast")
@@ -436,7 +437,11 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
final BillingEvent event2 = createMockBillingEvent(new LocalDate(2014, 10, 16).toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.<Usage>emptyList());
- final ContiguousIntervalUsageInArrear intervalConsumableInArrear = new ContiguousIntervalUsageInArrear(usage, accountId, invoiceId, rawUsages, targetDate, rawUsageStartDate, usageDetailMode, internalCallContext);
+
+ final ContiguousIntervalUsageInArrear intervalConsumableInArrear = usage.getUsageType() == UsageType.CAPACITY ?
+ new ContiguousIntervalCapacityUsageInArrear(usage, accountId, invoiceId, rawUsages, targetDate, rawUsageStartDate, usageDetailMode, internalCallContext) :
+ new ContiguousIntervalConsumableUsageInArrear(usage, accountId, invoiceId, rawUsages, targetDate, rawUsageStartDate, usageDetailMode, internalCallContext);
+
intervalConsumableInArrear.addBillingEvent(event1);
intervalConsumableInArrear.addBillingEvent(event2);
@@ -445,7 +450,7 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
}
@Test(groups = "fast")
- public void testTobeBilledForUnit() throws CatalogApiException {
+ public void testBilledUsage() throws CatalogApiException {
final DefaultTieredBlock block1 = createDefaultTieredBlock("cell-phone-minutes", 1000, 10000, new BigDecimal("0.5"));
final DefaultTieredBlock block2 = createDefaultTieredBlock("Mbytes", 512, 512000, new BigDecimal("0.3"));
@@ -453,32 +458,36 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
final DefaultUsage usage = createConsumableInArrearUsage(usageName, BillingPeriod.MONTHLY, TierBlockPolicy.ALL_TIERS, tier);
final LocalDate targetDate = new LocalDate(2014, 03, 20);
- final ContiguousIntervalUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
+ final ContiguousIntervalConsumableUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.<RawUsage>of(), targetDate, false,
createMockBillingEvent(targetDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),
BillingPeriod.MONTHLY,
Collections.<Usage>emptyList())
);
- List<UsageInArrearDetail> results = Lists.newArrayList();
- results.addAll(intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("cell-phone-minutes", 1000L)));
- results.addAll(intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("Mbytes", 30720L)));
- assertEquals(results.size(), 2);
+ final List<UsageConsumableInArrearTierUnitDetail> tierUnitDetails = Lists.newArrayList();
+ tierUnitDetails.addAll(intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("cell-phone-minutes", 1000L), ImmutableList.<UsageConsumableInArrearTierUnitDetail>of(), true));
+ tierUnitDetails.addAll(intervalConsumableInArrear.computeToBeBilledConsumableInArrear(new DefaultRolledUpUnit("Mbytes", 30720L), ImmutableList.<UsageConsumableInArrearTierUnitDetail>of(), true));
+ assertEquals(tierUnitDetails.size(), 2);
- assertEquals(intervalConsumableInArrear.toBeBilledForUnit(results), new BigDecimal("18.5"));
+ final UsageConsumableInArrearDetail details = new UsageConsumableInArrearDetail(tierUnitDetails);
+
+ assertEquals(details.getAmount().compareTo(new BigDecimal("18.5")), 0);
}
@Test(groups = "fast")
- public void testComputeMissingItemsAggregateModeAllTier() throws CatalogApiException, IOException {
+ public void testComputeMissingItemsAggregateModeAllTier() throws CatalogApiException, IOException, InvoiceApiException {
// Case 1
List<RawUsage> rawUsages = new ArrayList<RawUsage>();
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 99L));
- List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.AGGREGATE);
+ List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.AGGREGATE, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 1);
- assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("203")),0);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("203")), 0);
+
+ UsageConsumableInArrearDetail usageDetail = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<UsageConsumableInArrearDetail>() {});
+ List<UsageConsumableInArrearTierUnitDetail> itemDetails = usageDetail.getTierDetails();
- List<UsageInArrearDetail> itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
// BAR: 99 * 2 = 198
assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
assertEquals(itemDetails.get(0).getTier(), 1);
@@ -497,11 +506,13 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
- result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.AGGREGATE);
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.AGGREGATE, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 1);
- assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("225")),0);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("225")), 0);
+
+ usageDetail = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<UsageConsumableInArrearDetail>() {});
+ itemDetails = usageDetail.getTierDetails();
- itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
// BAR: 100 * 2 = 200
assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
assertEquals(itemDetails.get(0).getTier(), 1);
@@ -526,11 +537,12 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 75L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
- result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.AGGREGATE);
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.AGGREGATE, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 1);
- assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("2230")),0);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("2230")), 0);
- itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ usageDetail = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<UsageConsumableInArrearDetail>() {});
+ itemDetails = usageDetail.getTierDetails();
// BAR: 100 * 2 = 200
assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
assertEquals(itemDetails.get(0).getTier(), 1);
@@ -564,14 +576,14 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
}
@Test(groups = "fast")
- public void testComputeMissingItemsDetailModeAllTier() throws CatalogApiException, IOException {
+ public void testComputeMissingItemsDetailModeAllTier() throws CatalogApiException, IOException, InvoiceApiException {
// Case 1
List<RawUsage> rawUsages = new ArrayList<RawUsage>();
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 99L));
- List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.DETAIL);
+ List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.DETAIL, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 2);
// BAR: 99 * 2 = 198
assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("198")), 0);
@@ -587,7 +599,7 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
- result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.DETAIL);
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.DETAIL, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 3);
// BAR: 100 * 2 = 200
assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("200.0")), 0);
@@ -607,7 +619,7 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 75L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
- result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.DETAIL);
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.DETAIL, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 5);
// BAR: 100 * 2 = 200
assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("200.0")), 0);
@@ -632,18 +644,19 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
}
@Test(groups = "fast")
- public void testComputeMissingItemsAggregateModeTopTier() throws CatalogApiException, IOException {
+ public void testComputeMissingItemsAggregateModeTopTier() throws CatalogApiException, IOException, InvoiceApiException {
// Case 1
List<RawUsage> rawUsages = new ArrayList<RawUsage>();
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 99L));
- List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.AGGREGATE);
+ List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.AGGREGATE, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 1);
- assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("203")),0);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("203")), 0);
- List<UsageInArrearDetail> itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ UsageConsumableInArrearDetail usageDetail = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<UsageConsumableInArrearDetail>() {});
+ List<UsageConsumableInArrearTierUnitDetail> itemDetails = usageDetail.getTierDetails();
// BAR: 99 * 2 = 198
assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
assertEquals(itemDetails.get(0).getTier(), 1);
@@ -662,11 +675,13 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
- result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.AGGREGATE);
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.AGGREGATE, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 1);
- assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("2025")),0);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("2025")), 0);
+
+ usageDetail = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<UsageConsumableInArrearDetail>() {});
+ itemDetails = usageDetail.getTierDetails();
- itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
// BAR: 101 * 20 = 2020
assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
assertEquals(itemDetails.get(0).getTier(), 2);
@@ -685,11 +700,12 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 76L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
- result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.AGGREGATE);
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.AGGREGATE, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 1);
- assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("9620")),0);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("9620")), 0);
- itemDetails = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<List<UsageInArrearDetail>>() {});
+ usageDetail = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<UsageConsumableInArrearDetail>() {});
+ itemDetails = usageDetail.getTierDetails();
// BAR: 101 * 20 = 2020
assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
assertEquals(itemDetails.get(0).getTier(), 2);
@@ -705,14 +721,14 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
}
@Test(groups = "fast")
- public void testComputeMissingItemsDetailModeTopTier() throws CatalogApiException, IOException {
+ public void testComputeMissingItemsDetailModeTopTier() throws CatalogApiException, IOException, InvoiceApiException {
// Case 1
List<RawUsage> rawUsages = new ArrayList<RawUsage>();
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 99L));
- List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.DETAIL);
+ List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.DETAIL, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 2);
// BAR: 99 * 2 = 198
assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("198")), 0);
@@ -728,9 +744,9 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 5L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
- result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.DETAIL);
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.DETAIL, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 2);
- // BAR: 101 * 20 = 2020
+ // BAR: 101 * 20 = 2020
assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("2020.0")), 0);
assertEquals(result.get(0).getQuantity().intValue(), 101);
assertEquals(result.get(0).getRate().compareTo(new BigDecimal("20.0")), 0);
@@ -744,7 +760,7 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 76L));
rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 101L));
- result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.DETAIL);
+ result = produceInvoiceItems(rawUsages, TierBlockPolicy.TOP_TIER, UsageDetailMode.DETAIL, ImmutableList.<InvoiceItem>of());
assertEquals(result.size(), 2);
// BAR: 101 * 20 = 2020
assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("2020.0")), 0);
@@ -756,7 +772,82 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
assertEquals(result.get(1).getRate().compareTo(new BigDecimal("100.0")), 0);
}
- private List<InvoiceItem> produceInvoiceItems(List<RawUsage> rawUsages, TierBlockPolicy tierBlockPolicy, UsageDetailMode usageDetailMode) throws CatalogApiException {
+
+
+ @Test(groups = "fast")
+ public void testMultipleItemsAndTiersWithExistingItemsAllTiers() throws CatalogApiException, IOException, InvoiceApiException {
+
+ //
+ // Let's assume we were already billed on the previous period
+ //
+ // FOO : 10 (tier 1) + 40 (tier 2) = 50
+ final UsageConsumableInArrearTierUnitDetail existingFooUsageTier1 = new UsageConsumableInArrearTierUnitDetail(1, "FOO", BigDecimal.ONE, 1, 10, new BigDecimal("10.00"));
+ final UsageConsumableInArrearTierUnitDetail existingFooUsageTier2 = new UsageConsumableInArrearTierUnitDetail(2, "FOO", BigDecimal.TEN, 1, 40, new BigDecimal("400.00"));
+ // BAR : 10 (tier 1) + 40 (tier 2)
+ final UsageConsumableInArrearTierUnitDetail existingBarUsageTier1 = new UsageConsumableInArrearTierUnitDetail(1, "BAR", new BigDecimal("2.00"), 1, 80, new BigDecimal("160.00"));
+
+ final List<UsageConsumableInArrearTierUnitDetail> existingUsage = ImmutableList.of(existingFooUsageTier1, existingFooUsageTier2, existingBarUsageTier1);
+
+ final UsageConsumableInArrearDetail usageConsumableInArrearDetail = new UsageConsumableInArrearDetail(existingUsage);
+
+ final String existingUsageJson = objectMapper.writeValueAsString(usageConsumableInArrearDetail);
+
+ //
+ // Create usage data points (will include already billed + add new usage data)
+ //
+ List<RawUsage> rawUsages = new ArrayList<RawUsage>();
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 20), "FOO", 50L /* already built */ + 20L)); // tier 3
+ rawUsages.add(new DefaultRawUsage(subscriptionId, new LocalDate(2014, 03, 21), "BAR", 80L /* already built */ + 120L)); // tier 2
+
+ final List<InvoiceItem> existingItems = new ArrayList<InvoiceItem>();
+ final InvoiceItem ii1 = new UsageInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usageName, new LocalDate(2014, 03, 20), new LocalDate(2014, 04, 15), new BigDecimal("570.00"), null, currency, null, existingUsageJson);
+ existingItems.add(ii1);
+
+ List<InvoiceItem> result = produceInvoiceItems(rawUsages, TierBlockPolicy.ALL_TIERS, UsageDetailMode.AGGREGATE, existingItems);
+ assertEquals(result.size(), 1);
+ assertEquals(result.get(0).getAmount().compareTo(new BigDecimal("3140.00")), 0, String.format("%s != 3140.0", result.get(0).getAmount()));
+
+ UsageConsumableInArrearDetail usageDetail = objectMapper.readValue(result.get(0).getItemDetails(), new TypeReference<UsageConsumableInArrearDetail>() {});
+ List<UsageConsumableInArrearTierUnitDetail> itemDetails = usageDetail.getTierDetails();
+
+ // BAR item detail
+ assertEquals(itemDetails.get(0).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(0).getTier(), 1);
+ assertEquals(itemDetails.get(0).getTierBlockSize(), 1);
+ assertEquals(itemDetails.get(0).getQuantity().intValue(), 20);
+ assertEquals(itemDetails.get(0).getTierPrice().compareTo(new BigDecimal("2.00")), 0);
+ assertEquals(itemDetails.get(0).getAmount().compareTo(new BigDecimal("40.00")), 0);
+
+ assertEquals(itemDetails.get(1).getTierUnit(), "BAR");
+ assertEquals(itemDetails.get(1).getTier(), 2);
+ assertEquals(itemDetails.get(1).getTierBlockSize(), 1);
+ assertEquals(itemDetails.get(1).getQuantity().intValue(), 100);
+ assertEquals(itemDetails.get(1).getTierPrice().compareTo(new BigDecimal("20.00")), 0);
+ assertEquals(itemDetails.get(1).getAmount().compareTo(new BigDecimal("2000.00")), 0);
+
+ // FOO item detail
+ assertEquals(itemDetails.get(2).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(2).getTier(), 2);
+ assertEquals(itemDetails.get(2).getTierBlockSize(), 1);
+ assertEquals(itemDetails.get(2).getQuantity().intValue(), 10);
+ assertEquals(itemDetails.get(2).getTierPrice().compareTo(new BigDecimal("10.00")), 0);
+ assertEquals(itemDetails.get(2).getAmount().compareTo(new BigDecimal("100.00")), 0);
+
+ assertEquals(itemDetails.get(3).getTierUnit(), "FOO");
+ assertEquals(itemDetails.get(3).getTier(), 3);
+ assertEquals(itemDetails.get(3).getTierBlockSize(), 1);
+ assertEquals(itemDetails.get(3).getQuantity().intValue(), 10);
+ assertEquals(itemDetails.get(3).getTierPrice().compareTo(new BigDecimal("100.00")), 0);
+ assertEquals(itemDetails.get(3).getAmount().compareTo(new BigDecimal("1000.00")), 0);
+ }
+
+
+ @Test(groups = "fast")
+ public void testMultipleItemsAndTiersWithExistingItemsTopTier() throws CatalogApiException, IOException {
+ // TODO + code
+ }
+
+ private List<InvoiceItem> produceInvoiceItems(List<RawUsage> rawUsages, TierBlockPolicy tierBlockPolicy, UsageDetailMode usageDetailMode, final List<InvoiceItem> existingItems) throws CatalogApiException, InvoiceApiException {
final LocalDate startDate = new LocalDate(2014, 03, 20);
final LocalDate firstBCDDate = new LocalDate(2014, 04, 15);
@@ -782,7 +873,7 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
final ContiguousIntervalUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, rawUsages, targetDate, true, usageDetailMode, event1, event2);
- final UsageInArrearItemsAndNextNotificationDate usageResult = intervalConsumableInArrear.computeMissingItemsAndNextNotificationDate(ImmutableList.<InvoiceItem>of());
+ final UsageInArrearItemsAndNextNotificationDate usageResult = intervalConsumableInArrear.computeMissingItemsAndNextNotificationDate(existingItems);
final List<InvoiceItem> rawResults = usageResult.getInvoiceItems();
final List<InvoiceItem> result = ImmutableList.copyOf(Iterables.filter(rawResults, new Predicate<InvoiceItem>() {
@Override
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java
index f5c5c60..42c1709 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java
@@ -22,7 +22,6 @@ import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.catalog.DefaultInternationalPrice;
@@ -45,11 +44,10 @@ import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.usage.RawUsage;
import org.killbill.billing.util.config.definition.InvoiceConfig.UsageDetailMode;
+import org.killbill.billing.util.jackson.ObjectMapper;
import org.mockito.Mockito;
import org.testng.annotations.BeforeClass;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
public abstract class TestUsageInArrearBase extends InvoiceTestSuiteNoDB {
protected int BCD;
@@ -80,12 +78,27 @@ public abstract class TestUsageInArrearBase extends InvoiceTestSuiteNoDB {
objectMapper = new ObjectMapper();
}
- protected ContiguousIntervalUsageInArrear createContiguousIntervalConsumableInArrear(final DefaultUsage usage, final List<RawUsage> rawUsages, final LocalDate targetDate, final boolean closedInterval, final BillingEvent... events) {
+
+ protected ContiguousIntervalCapacityUsageInArrear createContiguousIntervalCapacityInArrear(final DefaultUsage usage, final List<RawUsage> rawUsages, final LocalDate targetDate, final boolean closedInterval, final BillingEvent... events) {
+ return createContiguousIntervalCapacityInArrear(usage, rawUsages, targetDate, closedInterval, usageDetailMode, events);
+ }
+
+ protected ContiguousIntervalCapacityUsageInArrear createContiguousIntervalCapacityInArrear(final DefaultUsage usage, final List<RawUsage> rawUsages, final LocalDate targetDate, final boolean closedInterval, UsageDetailMode detailMode, final BillingEvent... events) {
+ final ContiguousIntervalCapacityUsageInArrear intervalCapacityInArrear = new ContiguousIntervalCapacityUsageInArrear(usage, accountId, invoiceId, rawUsages, targetDate, new LocalDate(events[0].getEffectiveDate()), detailMode, internalCallContext);
+ for (final BillingEvent event : events) {
+ intervalCapacityInArrear.addBillingEvent(event);
+ }
+ intervalCapacityInArrear.build(closedInterval);
+ return intervalCapacityInArrear;
+ }
+
+
+ protected ContiguousIntervalConsumableUsageInArrear createContiguousIntervalConsumableInArrear(final DefaultUsage usage, final List<RawUsage> rawUsages, final LocalDate targetDate, final boolean closedInterval, final BillingEvent... events) {
return createContiguousIntervalConsumableInArrear(usage, rawUsages, targetDate, closedInterval, usageDetailMode, events);
}
- protected ContiguousIntervalUsageInArrear createContiguousIntervalConsumableInArrear(final DefaultUsage usage, final List<RawUsage> rawUsages, final LocalDate targetDate, final boolean closedInterval, UsageDetailMode detailMode, final BillingEvent... events) {
- final ContiguousIntervalUsageInArrear intervalConsumableInArrear = new ContiguousIntervalUsageInArrear(usage, accountId, invoiceId, rawUsages, targetDate, new LocalDate(events[0].getEffectiveDate()), detailMode, internalCallContext);
+ protected ContiguousIntervalConsumableUsageInArrear createContiguousIntervalConsumableInArrear(final DefaultUsage usage, final List<RawUsage> rawUsages, final LocalDate targetDate, final boolean closedInterval, UsageDetailMode detailMode, final BillingEvent... events) {
+ final ContiguousIntervalConsumableUsageInArrear intervalConsumableInArrear = new ContiguousIntervalConsumableUsageInArrear(usage, accountId, invoiceId, rawUsages, targetDate, new LocalDate(events[0].getEffectiveDate()), detailMode, internalCallContext);
for (final BillingEvent event : events) {
intervalConsumableInArrear.addBillingEvent(event);
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java
index a73a9b9..5e01d3c 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -18,12 +18,15 @@
package org.killbill.billing.jaxrs.resources;
+import java.util.Iterator;
+
import javax.inject.Inject;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
+import javax.ws.rs.HEAD;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
@@ -253,12 +256,21 @@ public class TestResource extends JaxRsResourceBase {
private boolean areAllNotificationsProcessed(final Long tenantRecordId) {
int nbNotifications = 0;
- for (final NotificationQueue notificationQueue : notificationQueueService.getNotificationQueues()) {
- for (final NotificationEventWithMetadata<NotificationEvent> notificationEvent : notificationQueue.getFutureOrInProcessingNotificationForSearchKey2(null, tenantRecordId)) {
- if (!notificationEvent.getEffectiveDate().isAfter(clock.getUTCNow())) {
- nbNotifications += 1;
+ final Iterator<NotificationQueue> iterator = notificationQueueService.getNotificationQueues().iterator();
+ try {
+ while (iterator.hasNext()) {
+ final NotificationQueue notificationQueue = iterator.next();
+ for (final NotificationEventWithMetadata<NotificationEvent> notificationEvent : notificationQueue.getFutureOrInProcessingNotificationForSearchKey2(null, tenantRecordId)) {
+ if (!notificationEvent.getEffectiveDate().isAfter(clock.getUTCNow())) {
+ nbNotifications += 1;
+ }
}
}
+ } finally {
+ // Go through all results to close the connection
+ while (iterator.hasNext()) {
+ iterator.next();
+ }
}
if (nbNotifications != 0) {
log.info("TestResource: {} queue(s) with more notification(s) to process", nbNotifications);
@@ -268,6 +280,7 @@ public class TestResource extends JaxRsResourceBase {
private boolean areAllBusEventsProcessed(final Long tenantRecordId) {
final Iterable<BusEventWithMetadata<BusEvent>> availableBusEventForSearchKey2 = persistentBus.getAvailableOrInProcessingBusEventsForSearchKey2(null, tenantRecordId);
+ // This will go through all results to close the connection
final int nbBusEvents = Iterables.size(availableBusEventForSearchKey2);
if (nbBusEvents != 0) {
log.info("TestResource: at least {} more bus event(s) to process", nbBusEvents);
NEWS 8(+7 -1)
diff --git a/NEWS b/NEWS
index 4827589..1be25b9 100644
--- a/NEWS
+++ b/NEWS
@@ -3,7 +3,13 @@
Fix connection leak (#558)
Fix limitation where catalog plan name cannot end with an number (#842)
Fix missing Invoice Notification when we have future billing events (#846)
- Rreduce log level of InvoiceItemGeneratorLogger (#851)
+ Reduce log level of InvoiceItemGeneratorLogger (#851)
+
+0.18.17
+ Relax sanity checks for STANDALONE subscriptions #840
+ Fix JDBC connection leak in pagination API #853
+ Fix limitation where catalog plan name cannot end with an number #842
+ Reduce log level of InvoiceItemGeneratorLogger #851
0.18.16
See https://github.com/killbill/killbill/releases/tag/killbill-0.18.16
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java
index d06c167..fc14f47 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -18,6 +18,7 @@
package org.killbill.billing.overdue.notification;
+import java.util.Iterator;
import java.util.UUID;
import org.joda.time.DateTime;
@@ -93,10 +94,18 @@ public abstract class DefaultOverduePosterBase implements OverduePoster {
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final Iterable<NotificationEventWithMetadata<T>> futureNotifications = getFutureNotificationsForAccountInTransaction(entitySqlDaoWrapperFactory, checkOverdueQueue,
clazz, context);
- for (final NotificationEventWithMetadata<T> notification : futureNotifications) {
- checkOverdueQueue.removeNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), notification.getRecordId());
+ final Iterator<NotificationEventWithMetadata<T>> iterator = futureNotifications.iterator();
+ try {
+ while (iterator.hasNext()) {
+ final NotificationEventWithMetadata<T> notification = iterator.next();
+ checkOverdueQueue.removeNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), notification.getRecordId());
+ }
+ } finally {
+ // Go through all results to close the connection
+ while (iterator.hasNext()) {
+ iterator.next();
+ }
}
-
return null;
}
});
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckPoster.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckPoster.java
index 25d7eff..3fc227a 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckPoster.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckPoster.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -18,6 +18,8 @@
package org.killbill.billing.overdue.notification;
+import java.util.Iterator;
+
import org.joda.time.DateTime;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
@@ -48,23 +50,32 @@ public class OverdueCheckPoster extends DefaultOverduePosterBase {
boolean shouldInsertNewNotification = true;
int minIndexToDeleteFrom = 0;
int index = 0;
- for (final NotificationEventWithMetadata<T> cur : futureNotifications) {
- // Results are ordered by effective date asc
- if (index == 0) {
- if (cur.getEffectiveDate().isBefore(futureNotificationTime)) {
- // We don't have to insert a new one. For sanity, delete any other future notification
- minIndexToDeleteFrom = 1;
- shouldInsertNewNotification = false;
- } else {
- // We win - we are before any other already recorded. Delete all others.
- minIndexToDeleteFrom = 0;
+ final Iterator<NotificationEventWithMetadata<T>> iterator = futureNotifications.iterator();
+ try {
+ while (iterator.hasNext()) {
+ final NotificationEventWithMetadata<T> cur = iterator.next();
+ // Results are ordered by effective date asc
+ if (index == 0) {
+ if (cur.getEffectiveDate().isBefore(futureNotificationTime)) {
+ // We don't have to insert a new one. For sanity, delete any other future notification
+ minIndexToDeleteFrom = 1;
+ shouldInsertNewNotification = false;
+ } else {
+ // We win - we are before any other already recorded. Delete all others.
+ minIndexToDeleteFrom = 0;
+ }
}
- }
- if (minIndexToDeleteFrom <= index) {
- overdueQueue.removeNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), cur.getRecordId());
+ if (minIndexToDeleteFrom <= index) {
+ overdueQueue.removeNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), cur.getRecordId());
+ }
+ index++;
+ }
+ } finally {
+ // Go through all results to close the connection
+ while (iterator.hasNext()) {
+ iterator.next();
}
- index++;
}
return shouldInsertNewNotification;
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java
index 1a7b3bd..ca991a2 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java
@@ -92,6 +92,7 @@ public class TestDefaultOverdueCheckPoster extends OverdueTestSuiteWithEmbeddedD
return entitySqlDaoTransactionalJdbiWrapper.execute(new EntitySqlDaoTransactionWrapper<List<NotificationEventWithMetadata<OverdueCheckNotificationKey>>>() {
@Override
public List<NotificationEventWithMetadata<OverdueCheckNotificationKey>> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+ // This will go through all results to close the connection
return ImmutableList.<NotificationEventWithMetadata<OverdueCheckNotificationKey>>copyOf(((OverdueCheckPoster) checkPoster).getFutureNotificationsForAccountInTransaction(entitySqlDaoWrapperFactory, overdueQueue, OverdueCheckNotificationKey.class, internalCallContext));
}
});
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
index 4bd018a..ecaa6d8 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -24,6 +24,7 @@ import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -411,11 +412,19 @@ public class PaymentProcessor extends ProcessorBase {
final Iterable<NotificationEventWithMetadata<NotificationEvent>> notificationEventWithMetadatas =
retryQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
- for (final NotificationEventWithMetadata<NotificationEvent> notificationEvent : notificationEventWithMetadatas) {
- if (((PaymentRetryNotificationKey) notificationEvent.getEvent()).getAttemptId().equals(lastPaymentAttemptId)) {
- retryQueue.removeNotification(notificationEvent.getRecordId());
+ final Iterator<NotificationEventWithMetadata<NotificationEvent>> iterator = notificationEventWithMetadatas.iterator();
+ try {
+ while (iterator.hasNext()) {
+ final NotificationEventWithMetadata<NotificationEvent> notificationEvent = iterator.next();
+ if (((PaymentRetryNotificationKey) notificationEvent.getEvent()).getAttemptId().equals(lastPaymentAttemptId)) {
+ retryQueue.removeNotification(notificationEvent.getRecordId());
+ }
}
+ } finally {
// Go through all results to close the connection
+ while (iterator.hasNext()) {
+ iterator.next();
+ }
}
} catch (final NoSuchNotificationQueue noSuchNotificationQueue) {
log.error("ERROR Loading Notification Queue - " + noSuchNotificationQueue.getMessage());
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
index 8e0f0fe..27a8bde 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -26,6 +26,7 @@ import java.util.UUID;
import org.joda.time.DateTime;
import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.FlakyRetryAnalyzer;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
import org.killbill.billing.payment.api.Payment;
@@ -94,7 +95,8 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(retrievedAttempts.get(0).getPluginName(), pluginName);
}
- @Test(groups = "slow")
+ // Flaky, see https://github.com/killbill/killbill/issues/860
+ @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
public void testPaymentAndTransactions() {
final UUID paymentMethodId = UUID.randomUUID();
final UUID accountId = UUID.randomUUID();
@@ -293,7 +295,8 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(deletedPaymentMethod.getPluginName(), pluginName);
}
- @Test(groups = "slow")
+ // Flaky, see https://github.com/killbill/killbill/issues/860
+ @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
public void testPendingTransactions() {
final UUID paymentMethodId = UUID.randomUUID();
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
index d09c923..086918e 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -20,6 +20,7 @@ package org.killbill.billing.payment;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -28,6 +29,7 @@ import java.util.concurrent.TimeUnit;
import org.joda.time.LocalDate;
import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.FlakyRetryAnalyzer;
import org.killbill.billing.api.TestApiListener;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.callcontext.InternalCallContext;
@@ -414,7 +416,8 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
}
// The test will check that when a PENDING entry stays PENDING, we go through all our retries and eventually give up (no infinite loop of retries)
- @Test(groups = "slow")
+ // Flaky, see https://github.com/killbill/killbill/issues/860
+ @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
public void testPendingEntriesThatDontMove() throws Exception {
final BigDecimal requestedAmount = BigDecimal.TEN;
@@ -447,13 +450,13 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
// Verify there is a notification to retry updating the value
assertEquals(getPendingNotificationCnt(internalCallContext), 1);
- clock.addDeltaFromReality(cur.getMillis() + 1);
+ clock.addDeltaFromReality(cur.getMillis() + 1000);
assertNotificationsCompleted(internalCallContext, 5);
// We add a sleep here to make sure the notification gets processed. Note that calling assertNotificationsCompleted alone would not work
// because there is a point in time where the notification queue is empty (showing notification was processed), but the processing of the notification
// will itself enter a new notification, and so the synchronization is difficult without writing *too much code*.
- Thread.sleep(1000);
+ Thread.sleep(1500);
assertNotificationsCompleted(internalCallContext, 5);
final Payment updatedPayment = paymentApi.getPayment(payment.getId(), false, false, ImmutableList.<PluginProperty>of(), callContext);
@@ -510,11 +513,19 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
@Override
public Boolean call() throws Exception {
boolean completed = true;
- for (final NotificationEventWithMetadata<NotificationEvent> notificationEvent : notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, Janitor.QUEUE_NAME).getFutureOrInProcessingNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId())) {
- if (!notificationEvent.getEffectiveDate().isAfter(clock.getUTCNow())) {
- completed = false;
+ final Iterator<NotificationEventWithMetadata<NotificationEvent>> iterator = notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, Janitor.QUEUE_NAME).getFutureOrInProcessingNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId()).iterator();
+ try {
+ while (iterator.hasNext()) {
+ final NotificationEventWithMetadata<NotificationEvent> notificationEvent = iterator.next();
+ if (!notificationEvent.getEffectiveDate().isAfter(clock.getUTCNow())) {
+ completed = false;
+ }
}
+ } finally {
// Go through all results to close the connection
+ while (iterator.hasNext()) {
+ iterator.next();
+ }
}
return completed;
}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java
index 4b32eba..5c53689 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -31,6 +31,7 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
+import org.killbill.billing.api.FlakyRetryAnalyzer;
import org.killbill.billing.client.KillBillClientException;
import org.killbill.billing.client.model.TenantKey;
import org.killbill.billing.jaxrs.json.NotificationJson;
@@ -152,7 +153,8 @@ public class TestPushNotification extends TestJaxrsBase {
return callback;
}
- @Test(groups = "slow")
+ // Flaky, see https://github.com/killbill/killbill/issues/860
+ @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
public void testPushNotificationRetries() throws Exception {
final String callback = registerTenantForCallback();
@@ -205,7 +207,8 @@ public class TestPushNotification extends TestJaxrsBase {
unregisterTenantForCallback(callback);
}
- @Test(groups = "slow")
+ // Flaky, see https://github.com/killbill/killbill/issues/860
+ @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
public void testPushNotificationRetriesMaxAttemptNumber() throws Exception {
final String callback = registerTenantForCallback();
@@ -231,16 +234,16 @@ public class TestPushNotification extends TestJaxrsBase {
resetCallbackStatusProperties();
- // move clock 15 minutes and get 1st retry
- clock.addDeltaFromReality(900000);
+ // move clock 15 minutes (+10s for flakiness) and get 1st retry
+ clock.addDeltaFromReality(910000);
assertAllCallbacksCompleted();
Assert.assertTrue(callbackCompletedWithError);
resetCallbackStatusProperties();
- // move clock an hour and get 2nd retry
- clock.addDeltaFromReality(3600000);
+ // move clock an hour (+10s for flakiness) and get 2nd retry
+ clock.addDeltaFromReality(3610000);
assertAllCallbacksCompleted();
Assert.assertTrue(callbackCompletedWithError);
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
index 3af29b0..c56df6e 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
@@ -295,7 +295,8 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
@Override
public SubscriptionBaseBundle inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
- final List<SubscriptionBundleModelDao> existingBundles = entitySqlDaoWrapperFactory.become(BundleSqlDao.class).getBundlesForLikeKey(bundle.getExternalKey(), context);
+ final List<SubscriptionBundleModelDao> existingBundles = bundle.getExternalKey() == null ? ImmutableList.<SubscriptionBundleModelDao>of()
+ : entitySqlDaoWrapperFactory.become(BundleSqlDao.class).getBundlesForLikeKey(bundle.getExternalKey(), context);
final SubscriptionBaseBundle unusedBundle = findExistingUnusedBundleForExternalKeyAndAccount(existingBundles, entitySqlDaoWrapperFactory);
if (unusedBundle != null) {
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg
index 369469b..ecfeb0f 100644
--- a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg
@@ -49,15 +49,10 @@ where id = :id
renameBundleExternalKey(prefix) ::= <<
-update bundles b
-join (select
- record_id
- , external_key
- from
- bundles
- where external_key = :externalKey <AND_CHECK_TENANT("")>) t
-on b.record_id = t.record_id
-set b.external_key = concat('kb', '<prefix>', '-', t.record_id, ':', t.external_key)
+update bundles
+set external_key = concat('kb', '<prefix>', '-', record_id, ':', external_key)
+where external_key = :externalKey
+<AND_CHECK_TENANT("")>
;
>>
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
index 4ed3a8f..8174045 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
@@ -24,6 +24,7 @@ import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
+import org.killbill.billing.api.FlakyRetryAnalyzer;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
@@ -205,7 +206,8 @@ public class TestUserApiCancel extends SubscriptionTestSuiteWithEmbeddedDB {
// Similar test to testCancelSubscriptionEOTWithChargeThroughDate except we uncancel and check things
// are as they used to be and we can move forward without hitting cancellation
- @Test(groups = "slow")
+ // Flaky, see https://github.com/killbill/killbill/issues/860
+ @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
public void testUncancel() throws SubscriptionBillingApiException, SubscriptionBaseApiException {
final String prod = "Shotgun";
final BillingPeriod term = BillingPeriod.MONTHLY;
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java
index 143041e..ba1229a 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
*
- * Ning licenses this file to you under the Apache License, version 2.0
+ * 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:
*
@@ -16,6 +18,7 @@
package org.killbill.billing.subscription.api.user;
+import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;
import java.util.List;
@@ -34,7 +37,6 @@ import org.killbill.billing.subscription.DefaultSubscriptionTestInitializer;
import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.events.phase.PhaseEvent;
-import org.mariadb.jdbc.internal.util.dao.QueryException;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -82,7 +84,7 @@ public class TestUserApiCreate extends SubscriptionTestSuiteWithEmbeddedDB {
subscriptionInternalApi.createBundleForAccount(bundle.getAccountId(), DefaultSubscriptionTestInitializer.DEFAULT_BUNDLE_KEY, false, internalCallContext);
Assert.fail("createBundleForAccount should fail because key already exists");
} catch (final RuntimeException e) {
- assertTrue(e.getCause() instanceof SQLIntegrityConstraintViolationException);
+ assertTrue(e.getCause() instanceof SQLException && (e.getCause() instanceof SQLIntegrityConstraintViolationException || "23505".compareTo(((SQLException) e.getCause()).getSQLState()) == 0));
}
final SubscriptionBaseBundle newBundle = subscriptionInternalApi.createBundleForAccount(bundle.getAccountId(), DefaultSubscriptionTestInitializer.DEFAULT_BUNDLE_KEY, true, internalCallContext);
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
index 04f0ab5..79bb057 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
@@ -25,6 +25,7 @@ import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.killbill.billing.ErrorCode;
+import org.killbill.billing.api.FlakyRetryAnalyzer;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
@@ -106,7 +107,8 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
}
}
- @Test(groups = "fast")
+ // Flaky, see https://github.com/killbill/killbill/issues/860
+ @Test(groups = "fast", retryAnalyzer = FlakyRetryAnalyzer.class)
public void testChangeSubscriptionNonActive() throws SubscriptionBaseApiException {
final SubscriptionBase subscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME);
util/src/main/resources/trimTenant.sql 75(+75 -0)
diff --git a/util/src/main/resources/trimTenant.sql b/util/src/main/resources/trimTenant.sql
new file mode 100644
index 0000000..d0bfb0c
--- /dev/null
+++ b/util/src/main/resources/trimTenant.sql
@@ -0,0 +1,75 @@
+drop procedure if exists trimTenant;
+DELIMITER //
+CREATE PROCEDURE trimTenant(p_api_key varchar(36))
+BEGIN
+
+ DECLARE v_tenant_record_id bigint /*! unsigned */;
+
+ select record_id from tenants WHERE api_key = p_api_key into v_tenant_record_id;
+
+ DELETE FROM analytics_account_fields WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_account_tags WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_account_transitions WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_accounts WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_bundle_fields WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_bundle_tags WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_bundles WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_invoice_adjustments WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_invoice_credits WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_invoice_fields WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_invoice_item_adjustments WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_invoice_items WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_invoice_payment_fields WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_invoice_tags WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_invoices WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_notifications WHERE search_key2 = v_tenant_record_id;
+ DELETE FROM analytics_notifications_history WHERE search_key2 = v_tenant_record_id;
+ DELETE FROM analytics_payment_auths WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_payment_captures WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_payment_chargebacks WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_payment_credits WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_payment_fields WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_payment_method_fields WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_payment_purchases WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_payment_refunds WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_payment_tags WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_payment_voids WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_subscription_transitions WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM analytics_transaction_fields WHERE tenant_record_id = v_tenant_record_id;
+
+ DELETE FROM account_email_history WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM account_emails WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM account_history WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM accounts WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM audit_log WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM blocking_states WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM bundles WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM bus_events WHERE search_key2 = v_tenant_record_id;
+ DELETE FROM bus_events_history WHERE search_key2 = v_tenant_record_id;
+ DELETE FROM bus_ext_events WHERE search_key2 = v_tenant_record_id;
+ DELETE FROM bus_ext_events_history WHERE search_key2 = v_tenant_record_id;
+ DELETE FROM custom_field_history WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM custom_fields WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM invoice_items WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM invoice_parent_children WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM invoice_payments WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM invoices WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM notifications WHERE search_key2 = v_tenant_record_id;
+ DELETE FROM notifications_history WHERE search_key2 = v_tenant_record_id;
+ DELETE FROM payment_attempt_history WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM payment_attempts WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM payment_history WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM payment_method_history WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM payment_methods WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM payment_transaction_history WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM payment_transactions WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM payments WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM rolled_up_usage WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM subscription_events WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM subscriptions WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM tag_history WHERE tenant_record_id = v_tenant_record_id;
+ DELETE FROM tags WHERE tenant_record_id = v_tenant_record_id;
+
+ END;
+//
+DELIMITER ;
diff --git a/util/src/test/java/org/killbill/billing/DBTestingHelper.java b/util/src/test/java/org/killbill/billing/DBTestingHelper.java
index 2808866..d940406 100644
--- a/util/src/test/java/org/killbill/billing/DBTestingHelper.java
+++ b/util/src/test/java/org/killbill/billing/DBTestingHelper.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -19,18 +19,27 @@
package org.killbill.billing;
import java.io.IOException;
+import java.io.PrintWriter;
import java.net.URL;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.SQLNonTransientConnectionException;
import java.util.Enumeration;
-import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.sql.DataSource;
import org.killbill.billing.platform.test.PlatformDBTestingHelper;
import org.killbill.billing.util.glue.IDBISetup;
import org.killbill.billing.util.io.IOUtils;
import org.killbill.commons.embeddeddb.EmbeddedDB;
+import org.killbill.commons.jdbi.guice.DBIProvider;
import org.skife.jdbi.v2.DBI;
import org.skife.jdbi.v2.IDBI;
import org.skife.jdbi.v2.ResultSetMapperFactory;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import com.google.common.base.MoreObjects;
@@ -38,7 +47,7 @@ public class DBTestingHelper extends PlatformDBTestingHelper {
private static DBTestingHelper dbTestingHelper = null;
- private AtomicBoolean initialized;
+ private DBI dbi;
public static synchronized DBTestingHelper get() {
if (dbTestingHelper == null) {
@@ -49,18 +58,18 @@ public class DBTestingHelper extends PlatformDBTestingHelper {
private DBTestingHelper() {
super();
- initialized = new AtomicBoolean(false);
}
@Override
- public IDBI getDBI() {
- final DBI dbi = (DBI) super.getDBI();
- // Register KB specific mappers
- if (initialized.compareAndSet(false, true)) {
+ public synchronized IDBI getDBI() {
+ if (dbi == null) {
+ final RetryableDataSource retryableDataSource = new RetryableDataSource(getDataSource());
+ dbi = (DBI) new DBIProvider(null, retryableDataSource, null).get();
+
+ // Register KB specific mappers
for (final ResultSetMapperFactory resultSetMapperFactory : IDBISetup.mapperFactoriesToRegister()) {
dbi.registerMapper(resultSetMapperFactory);
}
-
for (final ResultSetMapper resultSetMapper : IDBISetup.mappersToRegister()) {
dbi.registerMapper(resultSetMapper);
}
@@ -202,4 +211,73 @@ public class DBTestingHelper extends PlatformDBTestingHelper {
}
}
}
+
+ // DataSource which will retry recreating a connection once in case of a connection exception.
+ // This is useful for transient network errors in tests when using a separate database (e.g. Docker container),
+ // as we don't use a connection pool.
+ private static final class RetryableDataSource implements DataSource {
+
+ private static final Logger logger = LoggerFactory.getLogger(RetryableDataSource.class);
+
+ private final DataSource delegate;
+
+ private RetryableDataSource(final DataSource delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ try {
+ return delegate.getConnection();
+ } catch (final SQLNonTransientConnectionException e) {
+ logger.warn("Unable to retrieve connection, attempting to retry", e);
+ return delegate.getConnection();
+ }
+ }
+
+ @Override
+ public Connection getConnection(final String username, final String password) throws SQLException {
+ try {
+ return delegate.getConnection(username, password);
+ } catch (final SQLNonTransientConnectionException e) {
+ logger.warn("Unable to retrieve connection, attempting to retry", e);
+ return delegate.getConnection(username, password);
+ }
+ }
+
+ @Override
+ public <T> T unwrap(final Class<T> iface) throws SQLException {
+ return delegate.unwrap(iface);
+ }
+
+ @Override
+ public boolean isWrapperFor(final Class<?> iface) throws SQLException {
+ return delegate.isWrapperFor(iface);
+ }
+
+ @Override
+ public PrintWriter getLogWriter() throws SQLException {
+ return delegate.getLogWriter();
+ }
+
+ @Override
+ public void setLogWriter(final PrintWriter out) throws SQLException {
+ delegate.setLogWriter(out);
+ }
+
+ @Override
+ public void setLoginTimeout(final int seconds) throws SQLException {
+ delegate.setLoginTimeout(seconds);
+ }
+
+ @Override
+ public int getLoginTimeout() throws SQLException {
+ return delegate.getLoginTimeout();
+ }
+
+ //@Override
+ public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
+ throw new SQLFeatureNotSupportedException("javax.sql.DataSource.getParentLogger() is not currently supported by " + this.getClass().getName());
+ }
+ }
}