killbill-memoizeit
Changes
api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java 4(+4 -0)
api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java 6(+6 -0)
api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransition.java 4(+4 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java 7(+3 -4)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithDifferentBillingPeriods.java 69(+66 -3)
bin/gen_updater.rb 70(+70 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java 42(+0 -42)
entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java 11(+10 -1)
entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java 70(+60 -10)
entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java 12(+10 -2)
entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java 2(+2 -0)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java 152(+0 -152)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java 2(+1 -1)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java 82(+59 -23)
NEWS 11(+10 -1)
payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java 142(+142 -0)
payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCacheInvalidationCallback.java 49(+49 -0)
payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java 2(+1 -1)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java 59(+42 -17)
payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java 34(+24 -10)
payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java 54(+32 -22)
payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java 156(+7 -149)
payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java 52(+12 -40)
payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCache.java 148(+148 -0)
payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCacheInvalidationCallback.java 97(+97 -0)
payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentAutomatonDAOHelper.java 2(+1 -1)
payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java 53(+0 -53)
payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java 42(+41 -1)
profiles/killbill/pom.xml 5(+5 -0)
profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties 325(+129 -196)
profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties 199(+129 -70)
subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java 87(+85 -2)
subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java 13(+13 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java 4(+3 -1)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java 6(+4 -2)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java 46(+40 -6)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java 29(+29 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java 31(+30 -1)
subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java 2(+2 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java 64(+43 -21)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java 59(+44 -15)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java 1(+1 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java 9(+5 -4)
subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventBuilder.java 55(+55 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventData.java 65(+65 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java 3(+2 -1)
subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg 2(+2 -0)
subscription/src/main/resources/org/killbill/billing/subscription/migration/V20160915180903__cleanup_499.sql 4(+4 -0)
subscription/src/main/resources/org/killbill/billing/subscription/migration/V20160915180904__bcd_546.sql 2(+2 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java 6(+5 -1)
subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java 6(+6 -0)
usage/src/main/resources/org/killbill/billing/usage/migration/V20160915180905__tracking_id_502.sql 3(+3 -0)
util/src/main/java/org/killbill/billing/util/cache/TenantStateMachineConfigCacheLoader.java 94(+94 -0)
util/src/main/resources/ehcache.xml 18(+16 -2)
Details
diff --git a/api/src/main/java/org/killbill/billing/entitlement/EventsStream.java b/api/src/main/java/org/killbill/billing/entitlement/EventsStream.java
index 99363ed..42d9f4a 100644
--- a/api/src/main/java/org/killbill/billing/entitlement/EventsStream.java
+++ b/api/src/main/java/org/killbill/billing/entitlement/EventsStream.java
@@ -50,7 +50,6 @@ public interface EventsStream {
DateTime getEntitlementEffectiveEndDateTime();
-
SubscriptionBase getSubscriptionBase();
SubscriptionBase getBasePlanSubscriptionBase();
@@ -63,6 +62,8 @@ public interface EventsStream {
boolean isSubscriptionCancelled();
+ int getDefaultBillCycleDayLocal();
+
Collection<BlockingState> getPendingEntitlementCancellationEvents();
BlockingState getEntitlementCancellationEvent();
diff --git a/api/src/main/java/org/killbill/billing/events/SubscriptionInternalEvent.java b/api/src/main/java/org/killbill/billing/events/SubscriptionInternalEvent.java
index ea74974..1073d87 100644
--- a/api/src/main/java/org/killbill/billing/events/SubscriptionInternalEvent.java
+++ b/api/src/main/java/org/killbill/billing/events/SubscriptionInternalEvent.java
@@ -46,6 +46,8 @@ public interface SubscriptionInternalEvent extends BusInternalEvent {
String getPreviousPhase();
+ Integer getPreviousBillCycleDayLocal();
+
String getNextPlan();
String getNextPhase();
@@ -54,6 +56,8 @@ public interface SubscriptionInternalEvent extends BusInternalEvent {
String getNextPriceList();
+ Integer getNextBillCycleDayLocal();
+
Integer getRemainingEventsForUserOperation();
Long getTotalOrdering();
diff --git a/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java b/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java
index 0265dc1..f84d013 100644
--- a/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java
@@ -29,4 +29,5 @@ public interface BillingInternalApi {
* @return an ordered list of billing event for the given accounts
*/
public BillingEventSet getBillingEventsForAccountAndUpdateAccountBCD(UUID accountId, DryRunArguments dryRunArguments, InternalCallContext context) throws CatalogApiException, AccountApiException;
+
}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
index 07bec90..5382bba 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
@@ -100,9 +100,13 @@ public interface SubscriptionBase extends Entity, Blockable {
public ProductCategory getCategory();
+ public Integer getBillCycleDayLocal();
+
public SubscriptionBaseTransition getPendingTransition();
public SubscriptionBaseTransition getPreviousTransition();
public List<SubscriptionBaseTransition> getAllTransitions();
+
+ public DateTime getDateOfFirstRecurringNonZeroCharge();
}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
index a8ccdf6..2556f87 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
@@ -25,10 +25,13 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
@@ -96,4 +99,11 @@ public interface SubscriptionBaseInternalApi {
public Iterable<DateTime> getFutureNotificationsForAccount(InternalCallContext context);
public Map<UUID, DateTime> getNextFutureEventForSubscriptions(final SubscriptionBaseTransitionType eventType, final InternalCallContext internalCallContext);
+
+
+ public void updateBCD(final UUID subscriptionId, final int bcd, @Nullable final LocalDate effectiveFromDate, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException;
+
+ public int getDefaultBillCycleDayLocal(final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final PlanPhaseSpecifier planPhaseSpecifier, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal, final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException;
+
+
}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java
index 7a7729e..94606d0 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java
@@ -45,6 +45,10 @@ public enum SubscriptionBaseTransitionType {
*/
PHASE,
/**
+ * Update BCD for a specific subscription
+ */
+ BCD_CHANGE,
+ /**
* Generated by the system to mark the start of blocked billing overdue state. This is not on disk but computed by junction to create the billing events.
*/
START_BILLING_DISABLED,
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java
index dc56c4e..434b964 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java
@@ -73,5 +73,11 @@ public interface SubscriptionBaseTimeline extends Entity {
* @return the name of the phase
*/
public String getPlanPhaseName();
+
+ /**
+ *
+ * @return the new billCycleDayLocal
+ */
+ public Integer getBillCycleDayLocal();
}
}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransition.java b/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransition.java
index 035a349..508b495 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransition.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransition.java
@@ -62,5 +62,9 @@ public interface SubscriptionBaseTransition {
public SubscriptionBaseTransitionType getTransitionType();
+ public Integer getPreviousBillingCycleDayLocal();
+
+ public Integer getNextBillingCycleDayLocal();
+
public DateTime getCreatedDate();
}
diff --git a/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java b/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java
index 89c7cb7..73f06f0 100644
--- a/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * 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
@@ -53,8 +53,9 @@ public interface TenantInternalApi {
public String getPluginConfig(String pluginName, InternalTenantContext tenantContext);
+ public String getPluginPaymentStateMachineConfig(String pluginName, InternalTenantContext tenantContext);
+
public List<String> getTenantValuesForKey(final String key, final InternalTenantContext tenantContext);
public Tenant getTenantByApiKey(final String key) throws TenantApiException;
-
}
diff --git a/beatrix/src/main/java/org/killbill/billing/beatrix/extbus/BeatrixListener.java b/beatrix/src/main/java/org/killbill/billing/beatrix/extbus/BeatrixListener.java
index 1d2564f..b1a56b4 100644
--- a/beatrix/src/main/java/org/killbill/billing/beatrix/extbus/BeatrixListener.java
+++ b/beatrix/src/main/java/org/killbill/billing/beatrix/extbus/BeatrixListener.java
@@ -153,6 +153,8 @@ public class BeatrixListener {
eventBusType = ExtBusEventType.SUBSCRIPTION_CHANGE;
} else if (realEventST.getTransitionType() == SubscriptionBaseTransitionType.UNCANCEL) {
eventBusType = ExtBusEventType.SUBSCRIPTION_UNCANCEL;
+ } else if (realEventST.getTransitionType() == SubscriptionBaseTransitionType.BCD_CHANGE) {
+ eventBusType = ExtBusEventType.SUBSCRIPTION_BCD_CHANGE;
}
break;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/extbus/TestEventJson.java b/beatrix/src/test/java/org/killbill/billing/beatrix/extbus/TestEventJson.java
index 77ab028..1703097 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/extbus/TestEventJson.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/extbus/TestEventJson.java
@@ -27,7 +27,7 @@ import org.killbill.billing.ObjectType;
import org.killbill.billing.beatrix.BeatrixTestSuiteNoDB;
import org.killbill.billing.util.jackson.ObjectMapper;
-public class TestEventJson extends BeatrixTestSuiteNoDB {
+public class TestEventJson extends BeatrixTestSuiteNoDB {
private final ObjectMapper mapper = new ObjectMapper();
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index f1488ce..a01413c 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -595,8 +595,8 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 5, 1), callContext);
- // 2012-05-31 => DAY 30 have to get out of trial before first payment. A payment error, one for each invoice, should be on the bus (because there is no payment method)
- addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ // 2012-05-31 => DAY 30 have to get out of trial before first payment. An invoice payment error, one for each invoice, should be on the bus (because there is no payment method)
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT_ERROR);
invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 30), callContext);
@@ -611,8 +611,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-07-05 => DAY 65 - 35 days after invoice
- // Single PAYMENT_ERROR here here triggered by the invoice
- addDaysAndCheckForCompletion(20, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ addDaysAndCheckForCompletion(20, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT_ERROR);
invoiceChecker.checkInvoice(account.getId(), 3, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 7, 31), callContext);
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index cbbd5cf..50af600 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -204,6 +204,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
@Inject
protected SubscriptionApi subscriptionApi;
+
@Named(BeatrixIntegrationModule.NON_OSGI_PLUGIN_NAME)
@Inject
protected MockPaymentProviderPlugin paymentPlugin;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithDifferentBillingPeriods.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithDifferentBillingPeriods.java
index 3284c5b..52a8257 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithDifferentBillingPeriods.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithDifferentBillingPeriods.java
@@ -23,9 +23,6 @@ import java.util.List;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
-import org.killbill.billing.payment.api.PluginProperty;
-import org.testng.annotations.Test;
-
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.api.TestApiListener.NextEvent;
@@ -36,7 +33,9 @@ import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.util.tag.ControlTagType;
+import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
@@ -100,6 +99,70 @@ public class TestIntegrationWithDifferentBillingPeriods extends TestIntegrationB
checkNoMoreInvoiceToGenerate(account);
}
+
+ @Test(groups = "slow")
+ public void testChangeMonthlyToAnnualWithDifferentBCD() throws Exception {
+
+ // We take april as it has 30 days
+ final LocalDate today = new LocalDate(2016, 6, 1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(10));
+
+ // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+ clock.setDeltaFromReality(today.toDateTimeAtCurrentTime(DateTimeZone.UTC).getMillis() - clock.getUTCNow().getMillis());
+
+ final String productName = "Shotgun";
+
+ //
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE, NextEvent.BLOCK NextEvent.INVOICE
+ //
+
+ final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ assertNotNull(bpEntitlement);
+ assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 1);
+
+ assertEquals(bpEntitlement.getSubscriptionBase().getCurrentPlan().getRecurringBillingPeriod(), BillingPeriod.MONTHLY);
+
+ // Move out of trials for interesting invoices adjustments
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 2);
+ ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 1), new LocalDate(2016, 7, 10), InvoiceItemType.RECURRING, new BigDecimal("74.99")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+
+ // Invoice for a full month
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 3);
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 10), new LocalDate(2016, 8, 10), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+ //
+ // MOVE MONTHLY TO ANNUAL: Because of different Billing Alignment the BCD now becomes the 1 (first non null recurring phase)
+ //
+ changeEntitlementAndCheckForCompletion(bpEntitlement, productName, BillingPeriod.ANNUAL, BillingActionPolicy.IMMEDIATE, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 4);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 8, 1), new LocalDate(2017, 8, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 8, 1), new LocalDate(2016, 8, 10), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-72.57")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
+
+ checkNoMoreInvoiceToGenerate(account);
+ }
+
+
@Test(groups = "slow")
public void testChangeMonthlyToQuarterly() throws Exception {
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
index 8c5af5d..f3e26fe 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
@@ -50,7 +50,6 @@ import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.tweak.HandleCallback;
-import org.skife.jdbi.v2.tweak.VoidHandleCallback;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -58,6 +57,7 @@ import com.google.common.collect.ImmutableList;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
public class TestInvoicePayment extends TestIntegrationBase {
@@ -335,7 +335,7 @@ public class TestInvoicePayment extends TestIntegrationBase {
// Trigger chargeback in the original currency
payment1 = createChargeBackAndCheckForCompletion(account, payment1, new BigDecimal("225.44"), Currency.EUR, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
- Assert.assertEquals(payment1.getPurchasedAmount().compareTo(new BigDecimal("24.51")), 0);
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
Assert.assertEquals(payment1.getTransactions().size(), 2);
Assert.assertEquals(payment1.getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
Assert.assertEquals(payment1.getTransactions().get(0).getCurrency(), Currency.USD);
@@ -488,6 +488,7 @@ public class TestInvoicePayment extends TestIntegrationBase {
assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
assertEquals(invoice1.getPayments().get(0).getCurrency(), Currency.USD);
assertFalse(invoice1.getPayments().get(0).isSuccess());
+ assertNotNull(invoice1.getPayments().get(0).getPaymentId());
final BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
assertTrue(accountBalance1.compareTo(new BigDecimal("249.95")) == 0);
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
new file mode 100644
index 0000000..fcc3d52
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
@@ -0,0 +1,515 @@
+/*
+ * 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.beatrix.integration;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.junction.DefaultBlockingState;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import static org.testng.Assert.assertNotNull;
+
+public class TestWithBCDUpdate extends TestIntegrationBase {
+
+ @Inject
+ protected SubscriptionBaseInternalApi subscriptionBaseInternalApi;
+
+
+ @Test(groups = "slow")
+ public void testBCDChangeInTrial() throws Exception {
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-4-4 : (BP still in TRIAL)
+ clock.addDays(3);
+
+ // Set next BCD to be the 15
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15, null, internalCallContext);
+ Thread.sleep(1000);
+ assertListenerStatus();
+
+ // 2016-5-15 : Catch BCD_CHANGE event
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.NULL_INVOICE);
+ clock.addDays(11);
+ assertListenerStatus();
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(16);
+ assertListenerStatus();
+
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 1), new LocalDate(2016, 5, 15), InvoiceItemType.RECURRING, new BigDecimal("116.64")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2016-5-15 : NEW BCD
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(14);
+ assertListenerStatus();
+
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 15), new LocalDate(2016, 6, 15), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+ }
+
+
+ @Test(groups = "slow")
+ public void testBCDChangeAfterTrialFollowOtherBCDChange() throws Exception {
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ // Set next BCD to be the 15
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15, null, internalCallContext);
+ Thread.sleep(1000);
+ assertListenerStatus();
+
+ // 2016-5-15 : Catch BCD_CHANGE event and repair invoice accordingly
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(14);
+ assertListenerStatus();
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 15), new LocalDate(2016, 6, 15), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 15), new LocalDate(2016, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-137.07")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2016-6-01 : Original notification for 2016-6-01 (prior BCD change)
+ busHandler.pushExpectedEvents(NextEvent.NULL_INVOICE);
+ clock.addDays(17);
+ assertListenerStatus();
+
+
+ // 2016-6-15
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(14);
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 15), new LocalDate(2016, 7, 15), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // Set next BCD to be the 10
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10, null, internalCallContext);
+ Thread.sleep(1000);
+ assertListenerStatus();
+
+ // 2016-7-10
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(25);
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 10), new LocalDate(2016, 8, 10), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 10), new LocalDate(2016, 7, 15), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-41.66")));
+ invoiceChecker.checkInvoice(invoices.get(4).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+ }
+
+
+ @Test(groups = "slow")
+ public void testBCDChangeBeforeChangePlan() throws Exception {
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10, null, internalCallContext);
+
+ // 2016-5-5
+ clock.addDays(4);
+ changeEntitlementAndCheckForCompletion(baseEntitlement, "Assault-Rifle", BillingPeriod.MONTHLY, null, NextEvent.CHANGE, NextEvent.INVOICE);
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 5), new LocalDate(2016, 5, 10), InvoiceItemType.RECURRING, new BigDecimal("99.99")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 5), new LocalDate(2016, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-217.70")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 5), new LocalDate(2016, 5, 5), InvoiceItemType.CBA_ADJ, new BigDecimal("117.71")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2016-5-10
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(5);
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 10), new LocalDate(2016, 6, 10), InvoiceItemType.RECURRING, new BigDecimal("599.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 10), new LocalDate(2016, 5, 10), InvoiceItemType.CBA_ADJ, new BigDecimal("-117.71")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ }
+
+
+ @Test(groups = "slow")
+ public void testBCDChangeAfterChangePlan() throws Exception {
+
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ // 2016-5-5
+ clock.addDays(4);
+ changeEntitlementAndCheckForCompletion(baseEntitlement, "Assault-Rifle", BillingPeriod.MONTHLY, null, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 5), new LocalDate(2016, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("522.54")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 5), new LocalDate(2016, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-217.70")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10, null, internalCallContext);
+
+ // 2016-5-10
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(5);
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 10), new LocalDate(2016, 6, 10), InvoiceItemType.RECURRING, new BigDecimal("599.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 10), new LocalDate(2016, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-425.77")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ }
+
+ @Test(groups = "slow")
+ public void testBCDChangeForAnnualSubscriptionAndCancellation() throws Exception {
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.ANNUAL;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 10, null, internalCallContext);
+
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(9);
+ assertListenerStatus();
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 10), new LocalDate(2017, 5, 10), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 10), new LocalDate(2017, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2340.77")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2017, 5, 1 (at 13, 42, 0)
+ busHandler.pushExpectedEvents(NextEvent.NULL_INVOICE);
+ clock.setTime(new DateTime(2017, 5, 1, 0, 13, 42, 0, testTimeZone));
+ assertListenerStatus();
+
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ //clock.setDay(new LocalDate(2017, 5, 10));
+ clock.addDays(9);
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 5, 10), new LocalDate(2018, 5, 10), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ }
+
+
+ @Test(groups = "slow")
+ public void testBCDChangeForAO() throws Exception {
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-4-4 : (BP still in TRIAL)
+ // Laser-Scope has 1 month DISCOUNT
+ clock.addDays(3);
+ final DefaultEntitlement aoEntitlement = addAOEntitlementAndCheckForCompletion(baseEntitlement.getBundleId(), "Laser-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY,
+ NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ // 2016-5-1 : BP out of TRIAL + AO
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(27);
+ assertListenerStatus();
+
+ // 2016-5-4: Laser-Scope out of DISCOUNT
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(3);
+ assertListenerStatus();
+
+ // 2016-6-1 : BP + AO invoice
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(28);
+ assertListenerStatus();
+
+ // 2016-6-4 : Change BCD for AO and
+ clock.addDays(3);
+
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ subscriptionBaseInternalApi.updateBCD(aoEntitlement.getId(), 4, null, internalCallContext);
+ assertListenerStatus();
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = null;
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 4), new LocalDate(2016, 7, 4), InvoiceItemType.RECURRING, new BigDecimal("1999.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 4), new LocalDate(2016, 7, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-1799.96")));
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ invoiceChecker.checkInvoice(invoices.get(5).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2016-7-1 : BP only
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(27);
+ assertListenerStatus();
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 1), new LocalDate(2016, 8, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ invoiceChecker.checkInvoice(invoices.get(6).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2016-7-4 : AO only
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(3);
+ assertListenerStatus();
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 4), new LocalDate(2016, 8, 4), InvoiceItemType.RECURRING, new BigDecimal("1999.95")));
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ invoiceChecker.checkInvoice(invoices.get(7).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ checkNoMoreInvoiceToGenerate(account);
+ }
+
+ @Test(groups = "slow")
+ public void testBlockPastUnpaidPeriodAndRealignBCD() throws Exception {
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = null;
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+
+ paymentPlugin.makeNextPaymentFailWithError();
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ clock.addDays(30);
+ assertListenerStatus();
+
+
+ //
+ // Let's assume 15 days later, the customer comes back and wants to continue using the service (after he updated his payment method)
+ //
+ // The company 'a.b.c' decides to block both the billing and entitlement for the past 15 days and also move his BCD to
+ // the 16 so he gets to pay right away and for a full period (MONTHLY)
+ //
+ // 2016-5-16
+ busHandler.pushExpectedEvents(NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.PAYMENT_ERROR);
+ paymentPlugin.makeNextPaymentFailWithError();
+ clock.addDays(15);
+ assertListenerStatus();
+
+
+ // First BLOCK subscription starting from the 2016-5-1
+ // This will generate the credit for the full period, bringing by account balance to 0
+ busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.INVOICE);
+ final BlockingState blockingState = new DefaultBlockingState(baseEntitlement.getId(), BlockingStateType.SUBSCRIPTION, "COURTESY_BLOCK", "company.a.b.c", true, true, true, null);
+ subscriptionApi.addBlockingState(blockingState, new LocalDate(2016, 5, 1), ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 1), new LocalDate(2016, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-249.95")));
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 16), new LocalDate(2016, 5, 16), InvoiceItemType.CBA_ADJ, new BigDecimal("249.95")));
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // Second, move the BCD to the 16
+ // Because we did not unblock yet, we don't have a new invoice but we see the NULL_INVOICE event
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.NULL_INVOICE);
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 16, null, internalCallContext);
+ assertListenerStatus();
+
+ // Third, unblock starting at the 16, will generate a full period invoice
+ busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ final BlockingState unblockingState = new DefaultBlockingState(baseEntitlement.getId(), BlockingStateType.SUBSCRIPTION, "END_OF_COURTESY_BLOCK", "company.a.b.c", false, false, false, null);
+ subscriptionApi.addBlockingState(unblockingState, new LocalDate(2016, 5, 16), ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 16), new LocalDate(2016, 6, 16), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+ }
+
+
+ @Test(groups = "slow")
+ public void testBCDChangeWithEffectiveDateFromInTheFuture() throws Exception {
+
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ // BP creation : Will set Account BCD to the first (2016-4-1 + 30 days = 2016-5-1)
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2016-5-1 : BP out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ // Set next BCD to be the 15 but only starting from 2016-5-31
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15, new LocalDate(2016, 5, 31), internalCallContext);
+ Thread.sleep(1000);
+ assertListenerStatus();
+
+ // 2016-5-15 : We don't expect anything yet because of effectiveDateFrom = 2016-6-1
+ clock.addDays(14);
+ Thread.sleep(1000);
+ assertListenerStatus();
+
+ // 2016-6-1 : We expect a pro-ration from 2016-6-1 -> 2016-6-15
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(17);
+ assertListenerStatus();
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 1), new LocalDate(2016, 6, 15), InvoiceItemType.RECURRING, new BigDecimal("116.64")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // 2016-6-15 : Finally we get the BCD_CHANGE event and start building for full monthly period
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(14);
+ assertListenerStatus();
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 15), new LocalDate(2016, 7, 15), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
+ expectedInvoices.clear();
+ }
+}
bin/gen_updater.rb 70(+70 -0)
diff --git a/bin/gen_updater.rb b/bin/gen_updater.rb
new file mode 100644
index 0000000..14f75c9
--- /dev/null
+++ b/bin/gen_updater.rb
@@ -0,0 +1,70 @@
+require 'json'
+require 'open-uri'
+
+def get_as_json(url)
+ raw = URI.parse(url).read
+ JSON.parse(raw)
+end
+
+current_stable_train = nil
+current_dev_train = nil
+
+current_stable_version = nil
+current_dev_version = nil
+
+metadata = get_as_json("https://api.github.com/repos/killbill/killbill/tags")
+releases = []
+metadata.each do |entry|
+ parsed = entry['name'].scan(/killbill-([0-9]+\.([0-9]+)\.[0-9]+)/).last
+ version = parsed.first
+
+ train = parsed.last.to_i
+ if train % 2 == 1
+ current_dev_train = train if current_dev_train.to_i < train
+ current_dev_version = version if current_dev_version.nil? || (current_dev_version < version)
+ else
+ current_stable_train = train if current_stable_train.to_i < train
+ current_stable_version = version if current_stable_version.nil? || (current_stable_version < version)
+ end
+
+ releases << {
+ :train => train,
+ :version => version
+ }
+end
+
+doc =<<EOF
+## Top level keys
+# general.notice = This notice should rarely, if ever, be used as everyone will see it
+
+EOF
+
+current_train = nil
+latest_from_train = nil
+releases.each do |release|
+ if release[:train] != current_train || current_train == nil
+ current_train = release[:train]
+ latest_from_train = release[:version]
+ doc << "### 0.#{current_train}.x series ###\n\n"
+ end
+
+ doc << "\# #{release[:version]}\n"
+
+ if release[:version] == latest_from_train
+ doc << "#{release[:version]}.updates =\n"
+ else
+ doc << "#{release[:version]}.updates = #{latest_from_train}\n"
+ end
+
+ if release[:version] == current_dev_version || release[:version] == current_stable_version
+ doc << "#{release[:version]}.notices = This is the latest #{release[:train] % 2 == 1 ? 'dev' : 'GA'} release.\n"
+ elsif release[:train] != current_dev_train
+ doc << "#{release[:version]}.notices = We recommend upgrading to #{current_stable_version}, our latest GA release.\n"
+ else
+ doc << "#{release[:version]}.notices = We recommend upgrading to #{current_dev_version}, our latest dev release.\n"
+ end
+
+ doc << "#{release[:version]}.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-#{release[:version]}\n\n"
+end
+
+puts doc.chomp!
\ No newline at end of file
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
index f7898b5..2f66d29 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
@@ -16,9 +16,14 @@
package org.killbill.billing.catalog.api.user;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URI;
+
import javax.inject.Inject;
import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogService;
@@ -31,6 +36,7 @@ import org.killbill.billing.tenant.api.TenantUserApi;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.xmlloader.XMLLoader;
public class DefaultCatalogUserApi implements CatalogUserApi {
@@ -65,11 +71,17 @@ public class DefaultCatalogUserApi implements CatalogUserApi {
@Override
public void uploadCatalog(final String catalogXML, final CallContext callContext) throws CatalogApiException {
try {
+ // Validation purpose: Will throw if bad XML or catalog validation fails
+ final InputStream stream = new ByteArrayInputStream(catalogXML.getBytes());
+ XMLLoader.getObjectFromStream(new URI("dummy"), stream, StandaloneCatalog.class);
+
final InternalTenantContext internalTenantContext = createInternalTenantContext(callContext);
catalogCache.clearCatalog(internalTenantContext);
tenantApi.addTenantKeyValue(TenantKey.CATALOG.toString(), catalogXML, callContext);
} catch (TenantApiException e) {
throw new CatalogApiException(e);
+ } catch (final Exception e) {
+ throw new IllegalStateException(e);
}
}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
index f1ec38c..1cb7ffc 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
@@ -93,7 +93,6 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
// Refresh-able
protected EventsStream eventsStream;
-
public DefaultEntitlement(final UUID accountId, final UUID entitlementId, final EventsStreamBuilder eventsStreamBuilder,
final EntitlementApi entitlementApi, final EntitlementPluginExecution pluginExecution, final BlockingStateDao blockingStateDao,
final SubscriptionBaseInternalApi subscriptionInternalApi, final BlockingChecker checker,
@@ -279,6 +278,11 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
return getSubscriptionBase().getLastActiveCategory();
}
+ @Override
+ public Integer getBillCycleDayLocal() {
+ final Integer perSubscriptionBillCycleDayLocal = getSubscriptionBase().getBillCycleDayLocal();
+ return perSubscriptionBillCycleDayLocal != null ? perSubscriptionBillCycleDayLocal : eventsStream.getDefaultBillCycleDayLocal();
+ }
@Override
public Entitlement cancelEntitlementWithPolicy(final EntitlementActionPolicy entitlementPolicy, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
@@ -315,7 +319,6 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
properties,
callContext);
-
final WithEntitlementPlugin<Entitlement> cancelEntitlementWithPlugin = new WithEntitlementPlugin<Entitlement>() {
@Override
@@ -354,7 +357,6 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
return pluginExecution.executeWithPlugin(cancelEntitlementWithPlugin, pluginContext);
}
-
@Override
public void uncancelEntitlement(final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
@@ -400,7 +402,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
}
} else {
// Entitlement is NOT cancelled (or future cancelled), there is nothing to do
- throw new EntitlementApiException(ErrorCode.SUB_UNCANCEL_BAD_STATE, getId());
+ throw new EntitlementApiException(ErrorCode.ENT_UNCANCEL_BAD_STATE, getId());
}
// If billing was previously cancelled, reactivate
@@ -427,7 +429,6 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
return cancelEntitlementWithDateOverrideBillingPolicy(cancellationDate, billingPolicy, properties, callContext);
}
-
// See also EntitlementInternalApi#cancel for the bulk API
@Override
public Entitlement cancelEntitlementWithDateOverrideBillingPolicy(@Nullable final LocalDate entitlementEffectiveDate, final BillingActionPolicy billingPolicy, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
@@ -507,7 +508,6 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
@Override
public Entitlement changePlan(final String productName, final BillingPeriod billingPeriod, final String priceList, final List<PlanPhasePriceOverride> overrides, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
-
checkForPermissions(Permission.ENTITLEMENT_CAN_CHANGE_PLAN, callContext);
// Get the latest state from disk
@@ -694,6 +694,16 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
return pluginExecution.executeWithPlugin(changePlanWithPlugin, pluginContext);
}
+ @Override
+ public void updateBCD(final int newBCD, @Nullable final LocalDate effectiveFromDate, final CallContext callContext) throws EntitlementApiException {
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(getAccountId(), callContext);
+ try {
+ subscriptionInternalApi.updateBCD(getId(), newBCD, effectiveFromDate, context);
+ } catch (final SubscriptionBaseApiException e) {
+ throw new EntitlementApiException(e);
+ }
+ }
+
private void refresh(final TenantContext context) throws EntitlementApiException {
eventsStream = eventsStreamBuilder.refresh(eventsStream, context);
}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java
index 5899f53..b8df661 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java
@@ -165,22 +165,10 @@ public class DefaultEntitlementApiBase {
@Override
public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
try {
-
- final SubscriptionBaseBundle bundle = subscriptionInternalApi.getBundleFromId(bundleId, internalCallContext);
- final ImmutableAccountData account = accountApi.getImmutableAccountDataById(bundle.getAccountId(), internalCallContext);
final SubscriptionBase baseSubscription = subscriptionInternalApi.getBaseSubscription(bundleId, internalCallContext);
- final DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getBillingEffectiveDate(), internalCallContext);
-
- if (!dateHelper.isBeforeOrEqualsToday(effectiveDate, account.getTimeZone(), internalCallContext)) {
- recordPauseResumeNotificationEntry(baseSubscription.getId(), bundleId, effectiveDate, true, internalCallContext);
- return null;
- }
-
blockUnblockBundle(bundleId, DefaultEntitlementApi.ENT_STATE_BLOCKED, EntitlementService.ENTITLEMENT_SERVICE_NAME, localEffectiveDate, true, true, true, baseSubscription, internalCallContext);
} catch (SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
- } catch (AccountApiException e) {
- throw new EntitlementApiException(e);
}
return null;
}
@@ -205,22 +193,10 @@ public class DefaultEntitlementApiBase {
@Override
public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
try {
- final SubscriptionBaseBundle bundle = subscriptionInternalApi.getBundleFromId(bundleId, internalCallContext);
- final ImmutableAccountData account = accountApi.getImmutableAccountDataById(bundle.getAccountId(), internalCallContext);
final SubscriptionBase baseSubscription = subscriptionInternalApi.getBaseSubscription(bundleId, internalCallContext);
-
- final DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getBillingEffectiveDate(), internalCallContext);
-
- if (!dateHelper.isBeforeOrEqualsToday(effectiveDate, account.getTimeZone(), internalCallContext)) {
- recordPauseResumeNotificationEntry(baseSubscription.getId(), bundleId, effectiveDate, false, internalCallContext);
- return null;
- }
-
blockUnblockBundle(bundleId, DefaultEntitlementApi.ENT_STATE_CLEAR, EntitlementService.ENTITLEMENT_SERVICE_NAME, localEffectiveDate, false, false, false, baseSubscription, internalCallContext);
} catch (SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
- } catch (AccountApiException e) {
- throw new EntitlementApiException(e);
}
return null;
}
@@ -235,22 +211,4 @@ public class DefaultEntitlementApiBase {
entitlementUtils.setBlockingStatesAndPostBlockingTransitionEvent(ImmutableList.<BlockingState>of(state), bundleId, internalCallContext);
return state.getId();
}
-
- protected void recordPauseResumeNotificationEntry(final UUID entitlementId, final UUID bundleId, final DateTime effectiveDate, final boolean isPause, final InternalCallContext contextWithValidAccountRecordId) throws EntitlementApiException {
- final NotificationEvent notificationEvent = new EntitlementNotificationKey(entitlementId,
- bundleId,
- isPause ? EntitlementNotificationKeyAction.PAUSE : EntitlementNotificationKeyAction.RESUME,
- effectiveDate);
-
- try {
- final NotificationQueue subscriptionEventQueue = notificationQueueService.getNotificationQueue(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME,
- DefaultEntitlementService.NOTIFICATION_QUEUE_NAME);
- subscriptionEventQueue.recordFutureNotification(effectiveDate, notificationEvent, contextWithValidAccountRecordId.getUserToken(), contextWithValidAccountRecordId.getAccountRecordId(), contextWithValidAccountRecordId.getTenantRecordId());
- } catch (final NoSuchNotificationQueue e) {
- throw new EntitlementApiException(e, ErrorCode.__UNKNOWN_ERROR_CODE);
- } catch (final IOException e) {
- throw new EntitlementApiException(e, ErrorCode.__UNKNOWN_ERROR_CODE);
- }
- }
-
}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
index 8c21972..19d7cb9 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
@@ -70,6 +70,7 @@ public class DefaultEventsStream implements EventsStream {
private final List<SubscriptionBase> allSubscriptionsForBundle;
private final InternalTenantContext internalTenantContext;
private final DateTime utcNow;
+ private final int defaultBillCycleDayLocal;
private BlockingAggregator blockingAggregator;
private List<BlockingState> subscriptionEntitlementStates;
@@ -86,7 +87,9 @@ public class DefaultEventsStream implements EventsStream {
public DefaultEventsStream(final ImmutableAccountData account, final SubscriptionBaseBundle bundle,
final List<BlockingState> blockingStates, final BlockingChecker blockingChecker,
@Nullable final SubscriptionBase baseSubscription, final SubscriptionBase subscription,
- final List<SubscriptionBase> allSubscriptionsForBundle, final InternalTenantContext contextWithValidAccountRecordId, final DateTime utcNow) {
+ final List<SubscriptionBase> allSubscriptionsForBundle,
+ final int defaultBillCycleDayLocal,
+ final InternalTenantContext contextWithValidAccountRecordId, final DateTime utcNow) {
this.account = account;
this.bundle = bundle;
this.blockingStates = blockingStates;
@@ -94,6 +97,7 @@ public class DefaultEventsStream implements EventsStream {
this.baseSubscription = baseSubscription;
this.subscription = subscription;
this.allSubscriptionsForBundle = allSubscriptionsForBundle;
+ this.defaultBillCycleDayLocal = defaultBillCycleDayLocal;
this.internalTenantContext = contextWithValidAccountRecordId;
this.utcNow = utcNow;
@@ -194,6 +198,11 @@ public class DefaultEventsStream implements EventsStream {
}
@Override
+ public int getDefaultBillCycleDayLocal() {
+ return defaultBillCycleDayLocal;
+ }
+
+ @Override
public Collection<BlockingState> getBlockingStates() {
return blockingStates;
}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java
index dc6a097..6cee88b 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java
@@ -35,11 +35,17 @@ import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.AccountEventsStreams;
import org.killbill.billing.entitlement.EventsStream;
import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.api.EntitlementApiException;
import org.killbill.billing.entitlement.api.svcs.DefaultAccountEventsStreams;
import org.killbill.billing.entitlement.block.BlockingChecker;
@@ -88,7 +94,6 @@ public class EventsStreamBuilder {
this.checker = checker;
this.clock = clock;
this.internalCallContextFactory = internalCallContextFactory;
-
this.defaultBlockingStateDao = new DefaultBlockingStateDao(dbi, clock, notificationQueueService, eventBus, cacheControllerDispatcher, nonEntityDao, internalCallContextFactory);
this.blockingStateDao = new OptimizedProxyBlockingStateDao(this, subscriptionInternalApi, dbi, clock, notificationQueueService, eventBus, cacheControllerDispatcher, nonEntityDao, internalCallContextFactory);
}
@@ -324,15 +329,60 @@ public class EventsStreamBuilder {
final List<SubscriptionBase> allSubscriptionsForBundle,
final List<BlockingState> blockingStates,
final InternalTenantContext internalTenantContext) throws EntitlementApiException {
- return new DefaultEventsStream(account,
- bundle,
- blockingStates,
- checker,
- baseSubscription,
- subscription,
- allSubscriptionsForBundle,
- internalTenantContext,
- clock.getUTCNow());
+
+
+ try {
+ int accountBCD = accountInternalApi.getBCD(account.getId(), internalTenantContext);
+ int defaultAlignmentDay = subscriptionInternalApi.getDefaultBillCycleDayLocal(subscription, baseSubscription, createPlanPhaseSpecifier(subscription), account.getTimeZone(), accountBCD, clock.getUTCNow(), internalTenantContext);
+ return new DefaultEventsStream(account,
+ bundle,
+ blockingStates,
+ checker,
+ baseSubscription,
+ subscription,
+ allSubscriptionsForBundle,
+ defaultAlignmentDay,
+ internalTenantContext,
+ clock.getUTCNow());
+ } catch (final SubscriptionBaseApiException e) {
+ throw new EntitlementApiException(e);
+ } catch (final AccountApiException e) {
+ throw new EntitlementApiException(e);
+ }
+ }
+
+ private PlanPhaseSpecifier createPlanPhaseSpecifier(final SubscriptionBase subscription) {
+
+ final String lastActiveProductName;
+ final BillingPeriod billingPeriod;
+ final ProductCategory productCategory;
+ final String priceListName;
+ final PhaseType phaseType;
+
+ if (subscription.getState() == EntitlementState.PENDING) {
+ final SubscriptionBaseTransition transition = subscription.getPendingTransition();
+ final Product pendingProduct = transition.getNextPlan().getProduct();
+ lastActiveProductName = pendingProduct.getName();
+ productCategory = pendingProduct.getCategory();
+ final PlanPhase pendingPlanPhase = transition.getNextPhase();
+ billingPeriod = pendingPlanPhase.getRecurring() != null ? pendingPlanPhase.getRecurring().getBillingPeriod() : BillingPeriod.NO_BILLING_PERIOD;
+ priceListName = transition.getNextPriceList().getName();
+ phaseType = transition.getNextPhase().getPhaseType();
+ } else {
+ final Product lastActiveProduct = subscription.getLastActiveProduct();
+ lastActiveProductName = lastActiveProduct.getName();
+ productCategory = lastActiveProduct.getCategory();
+ final PlanPhase lastActivePlanPhase = subscription.getLastActivePhase();
+ billingPeriod = lastActivePlanPhase.getRecurring() != null ? lastActivePlanPhase.getRecurring().getBillingPeriod() : BillingPeriod.NO_BILLING_PERIOD;
+ priceListName = subscription.getLastActivePlan().getPriceList().getName();
+ phaseType = subscription.getLastActivePhase().getPhaseType();
+ }
+ return new PlanPhaseSpecifier(lastActiveProductName,
+ productCategory,
+ billingPeriod,
+ priceListName,
+ phaseType);
+
}
private SubscriptionBase findBaseSubscription(final Iterable<SubscriptionBase> subscriptions) {
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
index 62824a9..a963f6b 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
@@ -75,7 +75,7 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
entitlement.uncancelEntitlement(ImmutableList.<PluginProperty>of(), callContext);
Assert.fail("Entitlement hasn't been cancelled yet");
} catch (final EntitlementApiException e) {
- Assert.assertEquals(e.getCode(), ErrorCode.SUB_UNCANCEL_BAD_STATE.getCode());
+ Assert.assertEquals(e.getCode(), ErrorCode.ENT_UNCANCEL_BAD_STATE.getCode());
}
clock.addDays(3);
@@ -352,7 +352,7 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
}
@Test(groups = "slow", description = "Test pause / unpause in the future")
- public void testPauseUnpauseInTheFuture() throws AccountApiException, EntitlementApiException {
+ public void testPauseUnpauseInTheFuture() throws AccountApiException, EntitlementApiException, SubscriptionApiException {
final LocalDate initialDate = new LocalDate(2013, 8, 7);
clock.setDay(initialDate);
@@ -374,11 +374,19 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
// No event yet
assertListenerStatus();
+ final Entitlement refreshedAfterFuturePause = entitlementApi.getEntitlementForId(baseEntitlement.getId(), callContext);
+ assertEquals(refreshedAfterFuturePause.getState(), EntitlementState.ACTIVE);
+
+
final LocalDate resumeDate = new LocalDate(2013, 12, 24);
entitlementApi.resume(baseEntitlement.getBundleId(), resumeDate, ImmutableList.<PluginProperty>of(), callContext);
// No event yet
assertListenerStatus();
+ // Not worth writing another test in TestDefaultSubscriptionApi just for that subscription call. We want to check that future PAUSE/RESUME events are visible
+ final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), callContext);
+ Assert.assertEquals(subscription.getSubscriptionEvents().size(), 7);
+
testListener.pushExpectedEvents(NextEvent.BLOCK);
clock.setDay(pauseDate);
assertListenerStatus();
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
index c4dcbd3..dc0198b 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
@@ -1571,9 +1571,11 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
null,
null,
null,
+ null,
nextPlan,
nextPhase,
nextPriceList,
+ null,
1L,
createdDate,
UUID.randomUUID(),
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java b/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
index 17c6b9b..2db96ee 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
@@ -93,7 +93,7 @@ public abstract class InvoiceCalculatorUtils {
private static BigDecimal computeInvoiceAmountAdjustedForAccountCredit(final Currency currency, final Iterable<InvoiceItem> invoiceItems) {
BigDecimal amountAdjusted = BigDecimal.ZERO;
if (invoiceItems == null || !invoiceItems.iterator().hasNext()) {
- return amountAdjusted;
+ return KillBillMoney.of(amountAdjusted, currency);
}
for (final InvoiceItem invoiceItem : invoiceItems) {
@@ -119,7 +119,7 @@ public abstract class InvoiceCalculatorUtils {
public static BigDecimal computeInvoiceAmountCharged(final Currency currency, @Nullable final Iterable<InvoiceItem> invoiceItems) {
BigDecimal amountCharged = BigDecimal.ZERO;
if (invoiceItems == null || !invoiceItems.iterator().hasNext()) {
- return amountCharged;
+ return KillBillMoney.of(amountCharged, currency);
}
for (final InvoiceItem invoiceItem : invoiceItems) {
@@ -144,7 +144,7 @@ public abstract class InvoiceCalculatorUtils {
public static BigDecimal computeInvoiceOriginalAmountCharged(final DateTime invoiceCreatedDate, final Currency currency, @Nullable final Iterable<InvoiceItem> invoiceItems) {
BigDecimal amountCharged = BigDecimal.ZERO;
if (invoiceItems == null || !invoiceItems.iterator().hasNext()) {
- return amountCharged;
+ return KillBillMoney.of(amountCharged, currency);
}
for (final InvoiceItem invoiceItem : invoiceItems) {
@@ -160,7 +160,7 @@ public abstract class InvoiceCalculatorUtils {
public static BigDecimal computeInvoiceAmountCredited(final Currency currency, @Nullable final Iterable<InvoiceItem> invoiceItems) {
BigDecimal amountCredited = BigDecimal.ZERO;
if (invoiceItems == null || !invoiceItems.iterator().hasNext()) {
- return amountCredited;
+ return KillBillMoney.of(amountCredited, currency);
}
for (final InvoiceItem invoiceItem : invoiceItems) {
@@ -175,7 +175,7 @@ public abstract class InvoiceCalculatorUtils {
public static BigDecimal computeInvoiceAmountPaid(final Currency currency, @Nullable final Iterable<InvoicePayment> invoicePayments) {
BigDecimal amountPaid = BigDecimal.ZERO;
if (invoicePayments == null || !invoicePayments.iterator().hasNext()) {
- return amountPaid;
+ return KillBillMoney.of(amountPaid, currency);
}
for (final InvoicePayment invoicePayment : invoicePayments) {
@@ -193,7 +193,7 @@ public abstract class InvoiceCalculatorUtils {
public static BigDecimal computeInvoiceAmountRefunded(final Currency currency, @Nullable final Iterable<InvoicePayment> invoicePayments) {
BigDecimal amountRefunded = BigDecimal.ZERO;
if (invoicePayments == null || !invoicePayments.iterator().hasNext()) {
- return amountRefunded;
+ return KillBillMoney.of(amountRefunded, currency);
}
for (final InvoicePayment invoicePayment : invoicePayments) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index 3d54a46..89845e6 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -749,7 +749,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
// extract entries by invoiceId (which is always set, as opposed to paymentId) and then filter based on type and
// paymentCookieId = transactionExternalKey
//
- final List<InvoicePaymentModelDao> invoicePayments = transactional.getPaymentsForInvoice(invoicePayment.getInvoiceId().toString(), context);
+ final List<InvoicePaymentModelDao> invoicePayments = transactional.getAllPaymentsForInvoiceIncludedInit(invoicePayment.getInvoiceId().toString(), context);
final InvoicePaymentModelDao existingAttempt = Iterables.tryFind(invoicePayments, new Predicate<InvoicePaymentModelDao>() {
@Override
public boolean apply(final InvoicePaymentModelDao input) {
@@ -760,7 +760,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
if (existingAttempt == null) {
transactional.create(invoicePayment, context);
- } else if (!existingAttempt.getSuccess() && invoicePayment.getSuccess()) {
+ } else if (!existingAttempt.getSuccess()) {
transactional.updateAttempt(existingAttempt.getRecordId(),
invoicePayment.getPaymentId().toString(),
invoicePayment.getPaymentDate().toDate(),
@@ -769,7 +769,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
invoicePayment.getProcessedCurrency(),
invoicePayment.getPaymentCookieId(),
null,
- true,
+ invoicePayment.getSuccess(),
context);
}
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDaoHelper.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDaoHelper.java
index d4b939a..de91276 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDaoHelper.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDaoHelper.java
@@ -268,7 +268,7 @@ public class InvoiceDaoHelper {
private void getInvoicePaymentsWithinTransaction(final Iterable<InvoiceModelDao> invoices, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext context) {
final InvoicePaymentSqlDao invoicePaymentSqlDao = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class);
- final List<InvoicePaymentModelDao> invoicePaymentsForAccount = invoicePaymentSqlDao.getByAccountRecordId(context);
+ final List<InvoicePaymentModelDao> invoicePaymentsForAccount = invoicePaymentSqlDao.getByAccountRecordId(context);;
final Map<UUID, List<InvoicePaymentModelDao>> invoicePaymentsPerInvoiceId = new HashMap<UUID, List<InvoicePaymentModelDao>>();
for (final InvoicePaymentModelDao invoicePayment : invoicePaymentsForAccount) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
index 658dda0..d763bd1 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
@@ -36,12 +36,13 @@ import org.skife.jdbi.v2.sqlobject.SqlUpdate;
@EntitySqlDaoStringTemplate
public interface InvoicePaymentSqlDao extends EntitySqlDao<InvoicePaymentModelDao, InvoicePayment> {
+
@SqlQuery
public List<InvoicePaymentModelDao> getByPaymentId(@Bind("paymentId") final String paymentId,
@BindBean final InternalTenantContext context);
@SqlQuery
- public List<InvoicePaymentModelDao> getPaymentsForInvoice(@Bind("invoiceId") final String invoiceId,
+ public List<InvoicePaymentModelDao> getAllPaymentsForInvoiceIncludedInit(@Bind("invoiceId") final String invoiceId,
@BindBean final InternalTenantContext context);
@SqlQuery
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
index 13c569b..7fb6bb6 100644
--- a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
@@ -32,6 +32,18 @@ tableValues() ::= <<
, :createdDate
>>
+getByAccountRecordId(accountRecordId) ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+where (<accountRecordIdField("t.")> = :accountRecordId or (<accountRecordIdField("t.")> is null and :accountRecordId is null))
+<andCheckSoftDeletionWithComma("t.")>
+AND payment_id IS NOT NULL
+<AND_CHECK_TENANT("t.")>
+<defaultOrderBy("t.")>
+;
+>>
+
getByPaymentId() ::= <<
SELECT <allTableFields()>
FROM <tableName()>
@@ -51,7 +63,7 @@ getPaymentForCookieId() ::= <<
;
>>
-getPaymentsForInvoice() ::= <<
+getAllPaymentsForInvoiceIncludedInit() ::= <<
SELECT <allTableFields()>
FROM <tableName()>
WHERE invoice_id = :invoiceId
@@ -60,6 +72,8 @@ getPaymentsForInvoice() ::= <<
;
>>
+
+
getInvoicePayments() ::= <<
SELECT <allTableFields()>
FROM <tableName()>
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
index f0d823a..98fc4b3 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
@@ -278,8 +278,8 @@ public class TestDefaultInvoiceUserApi extends InvoiceTestSuiteWithEmbeddedDB {
final BigDecimal accountBalance = invoiceUserApi.getAccountBalance(accountId, callContext);
Assert.assertEquals(accountBalance, invoiceBalance);
- // Adjust the invoice for a fraction of the balance
- final BigDecimal adjAmount = invoiceItem.getAmount().divide(BigDecimal.TEN, BigDecimal.ROUND_HALF_UP);
+ // Adjust the invoice with most of the amount (-1 cent)
+ final BigDecimal adjAmount = invoiceItem.getAmount().subtract(new BigDecimal("0.01"));
final InvoiceItem adjInvoiceItem = invoiceUserApi.insertInvoiceItemAdjustment(accountId, invoiceId, invoiceItem.getId(),
clock.getUTCToday(), adjAmount, accountCurrency,
null, callContext);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/glue/DefaultJaxrsModule.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/glue/DefaultJaxrsModule.java
index da1f233..083a739 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/glue/DefaultJaxrsModule.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/glue/DefaultJaxrsModule.java
@@ -20,6 +20,7 @@ package org.killbill.billing.jaxrs.glue;
import org.killbill.billing.jaxrs.DefaultJaxrsService;
import org.killbill.billing.jaxrs.JaxrsExecutors;
import org.killbill.billing.jaxrs.JaxrsService;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
import org.killbill.billing.platform.api.KillbillConfigSource;
import org.killbill.billing.util.config.definition.JaxrsConfig;
import org.killbill.billing.util.glue.KillBillModule;
@@ -36,7 +37,7 @@ public class DefaultJaxrsModule extends KillBillModule {
final ConfigurationObjectFactory factory = new ConfigurationObjectFactory(skifeConfigSource);
final JaxrsConfig jaxrsConfig = factory.build(JaxrsConfig.class);
bind(JaxrsConfig.class).toInstance(jaxrsConfig);
-
+ bind(JaxrsUriBuilder.class).asEagerSingleton();
bind(JaxrsExecutors.class).asEagerSingleton();
bind(JaxrsService.class).to(DefaultJaxrsService.class).asEagerSingleton();
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountTimelineJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountTimelineJson.java
index 7537ada..0cb8603 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountTimelineJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountTimelineJson.java
@@ -26,6 +26,7 @@ import java.util.Set;
import java.util.UUID;
import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.entitlement.api.SubscriptionBundle;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceItem;
@@ -62,11 +63,11 @@ public class AccountTimelineJson {
final List<Payment> payments,
final List<InvoicePayment> invoicePayments,
final List<SubscriptionBundle> bundles,
- final AccountAuditLogs accountAuditLogs) {
+ final AccountAuditLogs accountAuditLogs) throws CatalogApiException {
this.account = new AccountJson(account, null, null, accountAuditLogs);
this.bundles = new LinkedList<BundleJson>();
for (final SubscriptionBundle bundle : bundles) {
- final BundleJson jsonWithSubscriptions = new BundleJson(bundle, accountAuditLogs);
+ final BundleJson jsonWithSubscriptions = new BundleJson(bundle, account.getCurrency(), accountAuditLogs);
this.bundles.add(jsonWithSubscriptions);
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
index 22effcd..0dbd4b5 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
@@ -23,6 +23,8 @@ import java.util.List;
import javax.annotation.Nullable;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.entitlement.api.Subscription;
import org.killbill.billing.entitlement.api.SubscriptionBundle;
import org.killbill.billing.util.audit.AccountAuditLogs;
@@ -56,18 +58,20 @@ public class BundleJson extends JsonBase {
this.timeline = timeline;
}
- public BundleJson(final SubscriptionBundle bundle, @Nullable final AccountAuditLogs accountAuditLogs) {
+ public BundleJson(final SubscriptionBundle bundle, @Nullable final Currency currency, @Nullable final AccountAuditLogs accountAuditLogs) throws CatalogApiException {
super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForBundle(bundle.getId())));
this.accountId = bundle.getAccountId().toString();
this.bundleId = bundle.getId().toString();
this.externalKey = bundle.getExternalKey();
this.subscriptions = new LinkedList<SubscriptionJson>();
for (final Subscription subscription : bundle.getSubscriptions()) {
- this.subscriptions.add(new SubscriptionJson(subscription, accountAuditLogs));
+ this.subscriptions.add(new SubscriptionJson(subscription, currency, accountAuditLogs));
}
this.timeline = new BundleTimelineJson(bundle.getTimeline(), accountAuditLogs);
}
+
+
public List<SubscriptionJson> getSubscriptions() {
return subscriptions;
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoicePaymentTransactionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoicePaymentTransactionJson.java
index 487825b..8c03d0e 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoicePaymentTransactionJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoicePaymentTransactionJson.java
@@ -41,6 +41,8 @@ public class InvoicePaymentTransactionJson extends PaymentTransactionJson {
@JsonProperty("amount") final BigDecimal amount,
@JsonProperty("currency") final String currency,
@JsonProperty("effectiveDate") final DateTime effectiveDate,
+ @JsonProperty("processedAmount") final BigDecimal processedAmount,
+ @JsonProperty("processedCurrency") final String processedCurrency,
@JsonProperty("status") final String status,
@JsonProperty("gatewayErrorCode") final String gatewayErrorCode,
@JsonProperty("gatewayErrorMsg") final String gatewayErrorMsg,
@@ -50,8 +52,8 @@ public class InvoicePaymentTransactionJson extends PaymentTransactionJson {
@JsonProperty("isAdjusted") final Boolean isAdjusted,
@JsonProperty("adjustments") final List<InvoiceItemJson> adjustments,
@JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
- super(transactionId, transactionExternalKey, paymentId, paymentExternalKey, transactionType, amount, currency, effectiveDate, status,
- gatewayErrorCode, gatewayErrorMsg, firstPaymentReferenceId, secondPaymentReferenceId, properties, auditLogs);
+ super(transactionId, transactionExternalKey, paymentId, paymentExternalKey, transactionType, amount, currency, effectiveDate, processedAmount, processedCurrency,
+ status, gatewayErrorCode, gatewayErrorMsg, firstPaymentReferenceId, secondPaymentReferenceId, properties, auditLogs);
this.isAdjusted = isAdjusted;
this.adjustments = adjustments;
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentTransactionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentTransactionJson.java
index 82a40dd..a9dff6a 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentTransactionJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentTransactionJson.java
@@ -49,6 +49,8 @@ public class PaymentTransactionJson extends JsonBase {
private final BigDecimal amount;
@ApiModelProperty(value = "Amount currency (account currency unless specified)", dataType = "org.killbill.billing.catalog.api.Currency")
private final String currency;
+ private final BigDecimal processedAmount;
+ private final String processedCurrency;
private final String gatewayErrorCode;
private final String gatewayErrorMsg;
// Plugin specific fields
@@ -65,6 +67,8 @@ public class PaymentTransactionJson extends JsonBase {
@JsonProperty("amount") final BigDecimal amount,
@JsonProperty("currency") final String currency,
@JsonProperty("effectiveDate") final DateTime effectiveDate,
+ @JsonProperty("processedAmount") final BigDecimal processedAmount,
+ @JsonProperty("processedCurrency") final String processedCurrency,
@JsonProperty("status") final String status,
@JsonProperty("gatewayErrorCode") final String gatewayErrorCode,
@JsonProperty("gatewayErrorMsg") final String gatewayErrorMsg,
@@ -82,6 +86,8 @@ public class PaymentTransactionJson extends JsonBase {
this.status = status;
this.amount = amount;
this.currency = currency;
+ this.processedAmount = processedAmount;
+ this.processedCurrency = processedCurrency;
this.gatewayErrorCode = gatewayErrorCode;
this.gatewayErrorMsg = gatewayErrorMsg;
this.firstPaymentReferenceId = firstPaymentReferenceId;
@@ -98,6 +104,8 @@ public class PaymentTransactionJson extends JsonBase {
transaction.getAmount(),
transaction.getCurrency() != null ? transaction.getCurrency().toString() : null,
transaction.getEffectiveDate(),
+ transaction.getProcessedAmount(),
+ transaction.getProcessedCurrency() != null ? transaction.getProcessedCurrency().toString() : null,
transaction.getTransactionStatus() != null ? transaction.getTransactionStatus().toString() : null,
transaction.getGatewayErrorCode(),
transaction.getGatewayErrorMsg(),
@@ -163,6 +171,14 @@ public class PaymentTransactionJson extends JsonBase {
return paymentExternalKey;
}
+ public BigDecimal getProcessedAmount() {
+ return processedAmount;
+ }
+
+ public String getProcessedCurrency() {
+ return processedCurrency;
+ }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("PaymentTransactionJson{");
@@ -174,6 +190,8 @@ public class PaymentTransactionJson extends JsonBase {
sb.append(", status='").append(status).append('\'');
sb.append(", amount=").append(amount);
sb.append(", currency='").append(currency).append('\'');
+ sb.append(", processedAmount=").append(processedAmount);
+ sb.append(", processedCurrency='").append(processedCurrency).append('\'');
sb.append(", gatewayErrorCode='").append(gatewayErrorCode).append('\'');
sb.append(", gatewayErrorMsg='").append(gatewayErrorMsg).append('\'');
sb.append(", firstPaymentReferenceId='").append(firstPaymentReferenceId).append('\'');
@@ -197,9 +215,15 @@ public class PaymentTransactionJson extends JsonBase {
if (amount != null ? amount.compareTo(that.amount) != 0 : that.amount != null) {
return false;
}
+ if (processedAmount != null ? processedAmount.compareTo(that.processedAmount) != 0 : that.processedAmount != null) {
+ return false;
+ }
if (currency != null ? !currency.equals(that.currency) : that.currency != null) {
return false;
}
+ if (processedCurrency != null ? !processedCurrency.equals(that.processedCurrency) : that.processedCurrency != null) {
+ return false;
+ }
if (transactionExternalKey != null ? !transactionExternalKey.equals(that.transactionExternalKey) : that.transactionExternalKey != null) {
return false;
}
@@ -233,7 +257,6 @@ public class PaymentTransactionJson extends JsonBase {
if (transactionType != null ? !transactionType.equals(that.transactionType) : that.transactionType != null) {
return false;
}
-
return true;
}
@@ -247,6 +270,8 @@ public class PaymentTransactionJson extends JsonBase {
result = 31 * result + (status != null ? status.hashCode() : 0);
result = 31 * result + (amount != null ? amount.hashCode() : 0);
result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (processedAmount != null ? processedAmount.hashCode() : 0);
+ result = 31 * result + (processedCurrency != null ? processedCurrency.hashCode() : 0);
result = 31 * result + (gatewayErrorCode != null ? gatewayErrorCode.hashCode() : 0);
result = 31 * result + (gatewayErrorMsg != null ? gatewayErrorMsg.hashCode() : 0);
result = 31 * result + (firstPaymentReferenceId != null ? firstPaymentReferenceId.hashCode() : 0);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java
index 1ce8cbb..6f8745a 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java
@@ -90,7 +90,8 @@ public class PhasePriceOverrideJson {
final PhasePriceOverrideJson that = (PhasePriceOverrideJson) o;
- if (fixedPrice != null ? !fixedPrice.equals(that.fixedPrice) : that.fixedPrice != null) {
+
+ if (fixedPrice != null ? fixedPrice.compareTo(that.fixedPrice) != 0 : that.fixedPrice != null) {
return false;
}
if (phaseName != null ? !phaseName.equals(that.phaseName) : that.phaseName != null) {
@@ -99,7 +100,7 @@ public class PhasePriceOverrideJson {
if (phaseType != null ? !phaseType.equals(that.phaseType) : that.phaseType != null) {
return false;
}
- if (recurringPrice != null ? !recurringPrice.equals(that.recurringPrice) : that.recurringPrice != null) {
+ if (recurringPrice != null ? recurringPrice.compareTo(that.recurringPrice) != 0 : that.recurringPrice != null) {
return false;
}
return true;
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
index adb80de..a9208ec 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
@@ -18,6 +18,8 @@
package org.killbill.billing.jaxrs.json;
+import java.math.BigDecimal;
+import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -26,6 +28,9 @@ import javax.annotation.Nullable;
import org.joda.time.LocalDate;
import org.killbill.billing.ObjectType;
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.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.Product;
@@ -66,10 +71,10 @@ public class SubscriptionJson extends JsonBase {
private final LocalDate chargedThroughDate;
private final LocalDate billingStartDate;
private final LocalDate billingEndDate;
+ private final Integer billCycleDayLocal;
private final List<EventSubscriptionJson> events;
private final List<PhasePriceOverrideJson> priceOverrides;
-
public static class EventSubscriptionJson extends JsonBase {
private final String eventId;
@@ -290,6 +295,7 @@ public class SubscriptionJson extends JsonBase {
@JsonProperty("chargedThroughDate") @Nullable final LocalDate chargedThroughDate,
@JsonProperty("billingStartDate") @Nullable final LocalDate billingStartDate,
@JsonProperty("billingEndDate") @Nullable final LocalDate billingEndDate,
+ @JsonProperty("billCycleDayLocal") @Nullable final Integer billCycleDayLocal,
@JsonProperty("events") @Nullable final List<EventSubscriptionJson> events,
@JsonProperty("priceOverrides") final List<PhasePriceOverrideJson> priceOverrides,
@JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
@@ -306,6 +312,7 @@ public class SubscriptionJson extends JsonBase {
this.chargedThroughDate = chargedThroughDate;
this.billingStartDate = billingStartDate;
this.billingEndDate = billingEndDate;
+ this.billCycleDayLocal = billCycleDayLocal;
this.accountId = accountId;
this.bundleId = bundleId;
this.subscriptionId = subscriptionId;
@@ -314,7 +321,7 @@ public class SubscriptionJson extends JsonBase {
this.priceOverrides = priceOverrides;
}
- public SubscriptionJson(final Subscription subscription, @Nullable final AccountAuditLogs accountAuditLogs) {
+ public SubscriptionJson(final Subscription subscription, @Nullable final Currency currency, @Nullable final AccountAuditLogs accountAuditLogs) throws CatalogApiException {
super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForSubscription(subscription.getId())));
this.startDate = subscription.getEffectiveStartDate();
@@ -352,6 +359,7 @@ public class SubscriptionJson extends JsonBase {
this.chargedThroughDate = subscription.getChargedThroughDate();
this.billingStartDate = subscription.getBillingStartDate();
this.billingEndDate = subscription.getBillingEndDate();
+ this.billCycleDayLocal = subscription.getBillCycleDayLocal();
this.accountId = subscription.getAccountId().toString();
this.bundleId = subscription.getBundleId().toString();
this.subscriptionId = subscription.getId().toString();
@@ -360,8 +368,20 @@ public class SubscriptionJson extends JsonBase {
for (final SubscriptionEvent subscriptionEvent : subscription.getSubscriptionEvents()) {
this.events.add(new EventSubscriptionJson(subscriptionEvent, accountAuditLogs));
}
- // It may be nice to recreate the override that were applied on the plans associated with that subscription
- this.priceOverrides = new LinkedList<PhasePriceOverrideJson>();
+
+ // We fill the catalog info every time we get the currency from the account (even if this is not overridden Plan)
+ this.priceOverrides = new ArrayList<PhasePriceOverrideJson>();
+ if (currency != null) {
+ final Plan plan = subscription.getLastActivePlan();
+ if (plan != null) {
+ for (final PlanPhase cur : plan.getAllPhases()) {
+ final BigDecimal fixedPrice = cur.getFixed() != null ? cur.getFixed().getPrice().getPrice(currency) : null;
+ final BigDecimal recurringPrice = cur.getRecurring() != null ? cur.getRecurring().getRecurringPrice().getPrice(currency) : null;
+ final PhasePriceOverrideJson phase = new PhasePriceOverrideJson(cur.getName(), cur.getPhaseType().toString(), fixedPrice, recurringPrice);
+ priceOverrides.add(phase);
+ }
+ }
+ }
}
public String getAccountId() {
@@ -428,6 +448,10 @@ public class SubscriptionJson extends JsonBase {
return billingEndDate;
}
+ public Integer getBillCycleDayLocal() {
+ return billCycleDayLocal;
+ }
+
public List<EventSubscriptionJson> getEvents() {
return events;
}
@@ -455,6 +479,7 @@ public class SubscriptionJson extends JsonBase {
sb.append(", chargedThroughDate=").append(chargedThroughDate);
sb.append(", billingStartDate=").append(billingStartDate);
sb.append(", billingEndDate=").append(billingEndDate);
+ sb.append(", billCycleDayLocal=").append(billCycleDayLocal);
sb.append(", events=").append(events);
sb.append(", priceOverrides=").append(priceOverrides);
sb.append('}');
@@ -526,6 +551,9 @@ public class SubscriptionJson extends JsonBase {
if (priceOverrides != null ? !priceOverrides.equals(that.priceOverrides) : that.priceOverrides != null) {
return false;
}
+ if (billCycleDayLocal != null ? !billCycleDayLocal.equals(that.billCycleDayLocal) : that.billCycleDayLocal != null) {
+ return false;
+ }
return true;
}
@@ -547,6 +575,7 @@ public class SubscriptionJson extends JsonBase {
result = 31 * result + (chargedThroughDate != null ? chargedThroughDate.hashCode() : 0);
result = 31 * result + (billingStartDate != null ? billingStartDate.hashCode() : 0);
result = 31 * result + (billingEndDate != null ? billingEndDate.hashCode() : 0);
+ result = 31 * result + (billCycleDayLocal != null ? billCycleDayLocal.hashCode() : 0);
result = 31 * result + (events != null ? events.hashCode() : 0);
result = 31 * result + (priceOverrides != null ? priceOverrides.hashCode() : 0);
return result;
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
index 93c03db..0dde840 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
@@ -58,6 +58,7 @@ import org.killbill.billing.account.api.AccountData;
import org.killbill.billing.account.api.AccountEmail;
import org.killbill.billing.account.api.AccountUserApi;
import org.killbill.billing.account.api.MutableAccountData;
+import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
@@ -268,7 +269,7 @@ public class AccountResource extends JaxRsResourceBase {
final TenantContext tenantContext = context.createContext(request);
final UUID uuid = UUID.fromString(accountId);
- accountUserApi.getAccountById(uuid, tenantContext);
+ final Account account = accountUserApi.getAccountById(uuid, tenantContext);
final List<SubscriptionBundle> bundles = (externalKey != null) ?
subscriptionApi.getSubscriptionBundlesForAccountIdAndExternalKey(uuid, externalKey, tenantContext) :
@@ -277,7 +278,12 @@ public class AccountResource extends JaxRsResourceBase {
final Collection<BundleJson> result = Collections2.transform(bundles, new Function<SubscriptionBundle, BundleJson>() {
@Override
public BundleJson apply(final SubscriptionBundle input) {
- return new BundleJson(input, null);
+ try {
+ return new BundleJson(input, account.getCurrency(), null);
+ } catch (final CatalogApiException e) {
+ // Not the cleanest thing, but guava Api don't allow throw..
+ throw new RuntimeException(e);
+ }
}
});
return Response.status(Status.OK).entity(result).build();
@@ -385,7 +391,7 @@ public class AccountResource extends JaxRsResourceBase {
public Response getAccountTimeline(@PathParam("accountId") final String accountIdString,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@QueryParam(QUERY_PARALLEL) @DefaultValue("false") final Boolean parallel,
- @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, PaymentApiException, SubscriptionApiException, InvoiceApiException {
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, PaymentApiException, SubscriptionApiException, InvoiceApiException, CatalogApiException {
final TenantContext tenantContext = context.createContext(request);
final UUID accountId = UUID.fromString(accountIdString);
@@ -1079,8 +1085,8 @@ public class AccountResource extends JaxRsResourceBase {
@HeaderParam(HDR_COMMENT) final String comment,
@javax.ws.rs.core.Context final HttpServletRequest request,
@javax.ws.rs.core.Context final UriInfo uriInfo) throws CustomFieldApiException {
- return super.createCustomFields(UUID.fromString(id), customFields,
- context.createContext(createdBy, reason, comment, request), uriInfo);
+ return super.createCustomFields(UUID.fromString(id), customFields, context.createContext(createdBy, reason,
+ comment, request), uriInfo);
}
@TimedResource
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/BundleResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/BundleResource.java
index a09e5db..f32c0f3 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/BundleResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/BundleResource.java
@@ -41,9 +41,11 @@ import javax.ws.rs.core.UriInfo;
import org.joda.time.LocalDate;
import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountUserApi;
import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.EntitlementApi;
import org.killbill.billing.entitlement.api.EntitlementApiException;
@@ -114,10 +116,13 @@ public class BundleResource extends JaxRsResourceBase {
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid bundle id supplied"),
@ApiResponse(code = 404, message = "Bundle not found")})
public Response getBundle(@PathParam("bundleId") final String bundleId,
- @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException, AccountApiException, CatalogApiException {
final UUID id = UUID.fromString(bundleId);
- final SubscriptionBundle bundle = subscriptionApi.getSubscriptionBundle(id, context.createContext(request));
- final BundleJson json = new BundleJson(bundle, null);
+
+ final TenantContext tenantContext = this.context.createContext(request);
+ final SubscriptionBundle bundle = subscriptionApi.getSubscriptionBundle(id, tenantContext);
+ final Account account = accountUserApi.getAccountById(bundle.getAccountId(), tenantContext);
+ final BundleJson json = new BundleJson(bundle, account.getCurrency(), null);
return Response.status(Status.OK).entity(json).build();
}
@@ -127,9 +132,13 @@ public class BundleResource extends JaxRsResourceBase {
@ApiOperation(value = "Retrieve a bundle by external key", response = BundleJson.class)
@ApiResponses(value = {@ApiResponse(code = 404, message = "Bundle not found")})
public Response getBundleByKey(@QueryParam(QUERY_EXTERNAL_KEY) final String externalKey,
- @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
- final SubscriptionBundle bundle = subscriptionApi.getActiveSubscriptionBundleForExternalKey(externalKey, context.createContext(request));
- final BundleJson json = new BundleJson(bundle, null);
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException, AccountApiException, CatalogApiException {
+
+ final TenantContext tenantContext = this.context.createContext(request);
+
+ final SubscriptionBundle bundle = subscriptionApi.getActiveSubscriptionBundleForExternalKey(externalKey, tenantContext);
+ final Account account = accountUserApi.getAccountById(bundle.getAccountId(), tenantContext);
+ final BundleJson json = new BundleJson(bundle, account.getCurrency(), null);
return Response.status(Status.OK).entity(json).build();
}
@@ -155,7 +164,13 @@ public class BundleResource extends JaxRsResourceBase {
if (accountsAuditLogs.get().get(bundle.getAccountId()) == null) {
accountsAuditLogs.get().put(bundle.getAccountId(), auditUserApi.getAccountAuditLogs(bundle.getAccountId(), auditMode.getLevel(), tenantContext));
}
- return new BundleJson(bundle, accountsAuditLogs.get().get(bundle.getAccountId()));
+
+ try {
+ return new BundleJson(bundle, null, accountsAuditLogs.get().get(bundle.getAccountId()));
+ } catch (final CatalogApiException unused) {
+ // Does not happen because we pass a null Currency
+ throw new RuntimeException(unused);
+ }
}
},
nextPageUri);
@@ -185,7 +200,12 @@ public class BundleResource extends JaxRsResourceBase {
if (accountsAuditLogs.get().get(bundle.getAccountId()) == null) {
accountsAuditLogs.get().put(bundle.getAccountId(), auditUserApi.getAccountAuditLogs(bundle.getAccountId(), auditMode.getLevel(), tenantContext));
}
- return new BundleJson(bundle, accountsAuditLogs.get().get(bundle.getAccountId()));
+ try {
+ return new BundleJson(bundle, null, accountsAuditLogs.get().get(bundle.getAccountId()));
+ } catch (final CatalogApiException unused) {
+ // Does not happen because we pass a null Currency
+ throw new RuntimeException(unused);
+ }
}
},
nextPageUri);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
index 04f7f8b..a262d14 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
@@ -16,9 +16,6 @@
package org.killbill.billing.jaxrs.resources;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.net.URI;
import java.util.ArrayList;
import java.util.List;
@@ -38,7 +35,6 @@ import javax.ws.rs.core.UriInfo;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.killbill.billing.account.api.AccountUserApi;
-import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.VersionedCatalog;
import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
@@ -57,7 +53,6 @@ import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.clock.Clock;
import org.killbill.commons.metrics.TimedResource;
-import org.killbill.xmlloader.XMLLoader;
import org.killbill.xmlloader.XMLWriter;
import com.google.inject.Inject;
@@ -76,9 +71,6 @@ public class CatalogResource extends JaxRsResourceBase {
private final CatalogUserApi catalogUserApi;
- // Catalog API don't quite support multiple catalogs per tenant
- private static final String catalogName = "unused";
-
@Inject
public CatalogResource(final JaxrsUriBuilder uriBuilder,
final TagUserApi tagUserApi,
@@ -114,10 +106,6 @@ public class CatalogResource extends JaxRsResourceBase {
@HeaderParam(HDR_COMMENT) final String comment,
@javax.ws.rs.core.Context final HttpServletRequest request,
@javax.ws.rs.core.Context final UriInfo uriInfo) throws Exception {
- // Validation purpose: Will throw if bad XML or catalog validation fails
- final InputStream stream = new ByteArrayInputStream(catalogXML.getBytes());
- XMLLoader.getObjectFromStream(new URI(JaxrsResource.CATALOG_PATH), stream, StandaloneCatalog.class);
-
final CallContext callContext = context.createContext(createdBy, reason, comment, request);
catalogUserApi.uploadCatalog(catalogXML, callContext);
return uriBuilder.buildResponse(uriInfo, CatalogResource.class, null, null);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
index 57f12d3..d37e575 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
@@ -30,6 +30,7 @@ public interface JaxrsResource {
public static final String REGISTER_NOTIFICATION_CALLBACK = "registerNotificationCallback";
public static final String UPLOAD_PLUGIN_CONFIG = "uploadPluginConfig";
public static final String UPLOAD_PER_TENANT_CONFIG = "uploadPerTenantConfig";
+ public static final String UPLOAD_PLUGIN_PAYMENT_STATE_MACHINE_CONFIG = "uploadPluginPaymentStateMachineConfig";
public static final String USER_KEY_VALUE = "userKeyValue";
public static final String SEARCH = "search";
@@ -79,6 +80,7 @@ public interface JaxrsResource {
public static final String QUERY_ENTITLEMENT_POLICY = "entitlementPolicy";
public static final String QUERY_SEARCH_OFFSET = "offset";
public static final String QUERY_SEARCH_LIMIT = "limit";
+ public static final String QUERY_ENTITLEMENT_EFFECTIVE_FROM_DT = "effectiveFromDate";
public static final String QUERY_ACCOUNT_WITH_BALANCE = "accountWithBalance";
public static final String QUERY_ACCOUNT_WITH_BALANCE_AND_CBA = "accountWithBalanceAndCBA";
@@ -128,6 +130,8 @@ public interface JaxrsResource {
public static final String QUERY_AUDIT = "audit";
+ public static final String QUERY_BCD = "bcd";
+
public static final String QUERY_PARALLEL = "parallel";
public static final String QUERY_AUTO_COMMIT = "autoCommit";
@@ -257,5 +261,6 @@ public interface JaxrsResource {
public static final String CHILDREN = "children";
public static final String CHILDREN_PATH = PREFIX + "/" + CHILDREN;
+ public static final String BCD = "bcd";
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
index 0f4de38..ee5cf1a 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -117,6 +117,10 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
static final Logger log = LoggerFactory.getLogger(JaxRsResourceBase.class);
+ // Catalog API don't quite support multiple catalogs per tenant
+ protected static final String catalogName = "unused";
+
+
protected static final ObjectMapper mapper = new ObjectMapper();
protected final JaxrsUriBuilder uriBuilder;
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
index a74d09b..e770d05 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
@@ -47,6 +47,7 @@ import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountUserApi;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
@@ -141,10 +142,12 @@ public class SubscriptionResource extends JaxRsResourceBase {
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid subscription id supplied"),
@ApiResponse(code = 404, message = "Subscription not found")})
public Response getEntitlement(@PathParam("subscriptionId") final String subscriptionId,
- @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException, AccountApiException, CatalogApiException {
final UUID uuid = UUID.fromString(subscriptionId);
- final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(uuid, context.createContext(request));
- final SubscriptionJson json = new SubscriptionJson(subscription, null);
+ final TenantContext context = this.context.createContext(request);
+ final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(uuid, context);
+ final Account account = accountUserApi.getAccountById(subscription.getAccountId(), context);
+ final SubscriptionJson json = new SubscriptionJson(subscription, account.getCurrency(), null);
return Response.status(Status.OK).entity(json).build();
}
@@ -159,6 +162,7 @@ public class SubscriptionResource extends JaxRsResourceBase {
@QueryParam(QUERY_ENTITLEMENT_REQUESTED_DT) final String entitlementDate,
@QueryParam(QUERY_BILLING_REQUESTED_DT) final String billingDate,
@QueryParam(QUERY_MIGRATED) @DefaultValue("false") final Boolean isMigrated,
+ @QueryParam(QUERY_BCD) final Integer newBCD,
@QueryParam(QUERY_CALL_COMPLETION) @DefaultValue("false") final Boolean callCompletion,
@QueryParam(QUERY_CALL_TIMEOUT) @DefaultValue("3") final long timeoutSec,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
@@ -184,7 +188,6 @@ public class SubscriptionResource extends JaxRsResourceBase {
final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
final CallContext callContext = context.createContext(createdBy, reason, comment, request);
-
final EntitlementCallCompletionCallback<Entitlement> callback = new EntitlementCallCompletionCallback<Entitlement>() {
@Override
public Entitlement doOperation(final CallContext ctx) throws InterruptedException, TimeoutException, EntitlementApiException, SubscriptionApiException, AccountApiException {
@@ -202,9 +205,13 @@ public class SubscriptionResource extends JaxRsResourceBase {
BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList());
final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(entitlement.getPriceOverrides(), planSpec, account.getCurrency());
- return createAddOnEntitlement ?
- entitlementApi.addEntitlement(getBundleIdForAddOnCreation(entitlement), spec, overrides, resolvedEntitlementDate, resolvedBillingDate, isMigrated, pluginProperties, callContext) :
- entitlementApi.createBaseEntitlement(account.getId(), spec, entitlement.getExternalKey(), overrides, resolvedEntitlementDate, resolvedBillingDate, isMigrated, pluginProperties, callContext);
+ final Entitlement result = createAddOnEntitlement ?
+ entitlementApi.addEntitlement(getBundleIdForAddOnCreation(entitlement), spec, overrides, resolvedEntitlementDate, resolvedBillingDate, isMigrated, pluginProperties, callContext) :
+ entitlementApi.createBaseEntitlement(account.getId(), spec, entitlement.getExternalKey(), overrides, resolvedEntitlementDate, resolvedBillingDate, isMigrated, pluginProperties, callContext);
+ if (newBCD != null) {
+ result.updateBCD(newBCD, null, callContext);
+ }
+ return result;
}
private UUID getBundleIdForAddOnCreation(final SubscriptionJson entitlement) throws SubscriptionApiException {
@@ -283,11 +290,11 @@ public class SubscriptionResource extends JaxRsResourceBase {
final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
final SubscriptionJson baseEntitlement = Iterables.tryFind(entitlements, new Predicate<SubscriptionJson>() {
- @Override
- public boolean apply(final SubscriptionJson subscription) {
- return ProductCategory.BASE.toString().equalsIgnoreCase(subscription.getProductCategory());
- }
- }).orNull();
+ @Override
+ public boolean apply(final SubscriptionJson subscription) {
+ return ProductCategory.BASE.toString().equalsIgnoreCase(subscription.getProductCategory());
+ }
+ }).orNull();
verifyNonNull(baseEntitlement.getAccountId(), "SubscriptionJson accountId needs to be set for BASE product.");
@@ -308,8 +315,8 @@ public class SubscriptionResource extends JaxRsResourceBase {
BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), null);
final PlanSpecifier planSpec = new PlanSpecifier(entitlement.getProductName(),
- ProductCategory.valueOf(entitlement.getProductCategory()),
- BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList());
+ ProductCategory.valueOf(entitlement.getProductCategory()),
+ BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList());
final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(entitlement.getPriceOverrides(), planSpec, account.getCurrency());
EntitlementSpecifier specifier = new EntitlementSpecifier() {
@@ -436,7 +443,7 @@ public class SubscriptionResource extends JaxRsResourceBase {
}
@Override
- public Response doResponseOk(final Response operationResponse) throws SubscriptionApiException {
+ public Response doResponseOk(final Response operationResponse) throws SubscriptionApiException, AccountApiException, CatalogApiException {
if (operationResponse.getStatus() != Status.OK.getStatusCode()) {
return operationResponse;
}
@@ -448,7 +455,6 @@ public class SubscriptionResource extends JaxRsResourceBase {
return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
}
-
@TimedResource
@PUT
@Path("/{subscriptionId:" + UUID_PATTERN + "}/" + BLOCK)
@@ -468,8 +474,6 @@ public class SubscriptionResource extends JaxRsResourceBase {
return addBlockingState(json, id, BlockingStateType.SUBSCRIPTION, requestedDate, pluginPropertiesString, createdBy, reason, comment, request);
}
-
-
@TimedResource
@DELETE
@Path("/{subscriptionId:" + UUID_PATTERN + "}")
@@ -543,6 +547,34 @@ public class SubscriptionResource extends JaxRsResourceBase {
return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
}
+ @TimedResource
+ @PUT
+ @Produces(APPLICATION_JSON)
+ @Consumes(APPLICATION_JSON)
+ @Path("/{subscriptionId:" + UUID_PATTERN + "}/" + BCD)
+ @ApiOperation(value = "Update the BCD associated to a subscription")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid entitlement supplied")})
+ public Response updateSubscriptionBCD(final SubscriptionJson json,
+ @PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_ENTITLEMENT_EFFECTIVE_FROM_DT) final String effectiveFromDateStr,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final UriInfo uriInfo,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException, EntitlementApiException {
+
+ verifyNonNullOrEmpty(json, "SubscriptionJson body should be specified");
+ verifyNonNullOrEmpty(json.getBillCycleDayLocal(), "SubscriptionJson new BCD should be specified");
+
+ final LocalDate effectiveFromDate = toLocalDate(effectiveFromDateStr);
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+ final UUID subscriptionId = UUID.fromString(id);
+
+ final Entitlement entitlement = entitlementApi.getEntitlementForId(subscriptionId, callContext);
+ entitlement.updateBCD(json.getBillCycleDayLocal(), effectiveFromDate, callContext);
+ return Response.status(Status.OK).build();
+ }
+
private static final class CompletionUserRequestEntitlement extends CompletionUserRequestBase {
public CompletionUserRequestEntitlement(final UUID userToken) {
@@ -600,7 +632,7 @@ public class SubscriptionResource extends JaxRsResourceBase {
public boolean isImmOperation();
- public Response doResponseOk(final T operationResponse) throws SubscriptionApiException;
+ public Response doResponseOk(final T operationResponse) throws SubscriptionApiException, AccountApiException, CatalogApiException;
}
private class EntitlementCallCompletion<T> {
@@ -621,6 +653,8 @@ public class SubscriptionResource extends JaxRsResourceBase {
return callback.doResponseOk(operationValue);
} catch (final InterruptedException e) {
return Response.status(Status.INTERNAL_SERVER_ERROR).build();
+ } catch (final CatalogApiException e) {
+ throw new EntitlementApiException(e);
} catch (final TimeoutException e) {
return Response.status(408).build();
} finally {
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
index ff55211..5a3eb50 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
@@ -225,7 +225,6 @@ public class TenantResource extends JaxRsResourceBase {
return insertTenantKey(TenantKey.PER_TENANT_CONFIG, null, perTenantConfig, uriInfo, "getPerTenantConfiguration", createdBy, reason, comment, request);
}
-
@TimedResource
@GET
@Path("/" + UPLOAD_PER_TENANT_CONFIG)
@@ -248,6 +247,46 @@ public class TenantResource extends JaxRsResourceBase {
return deleteTenantKey(TenantKey.PER_TENANT_CONFIG, null, createdBy, reason, comment, request);
}
+ @TimedResource
+ @POST
+ @Path("/" + UPLOAD_PLUGIN_PAYMENT_STATE_MACHINE_CONFIG + "/{pluginName:" + ANYTHING_PATTERN + "}")
+ @Consumes(TEXT_PLAIN)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Add a per tenant payment state machine for a plugin")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid tenantId supplied")})
+ public Response uploadPluginPaymentStateMachineConfig(final String paymentStateMachineConfig,
+ @PathParam("pluginName") final String pluginName,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws TenantApiException {
+ return insertTenantKey(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_, pluginName, paymentStateMachineConfig, uriInfo, "getPluginPaymentStateMachineConfig", createdBy, reason, comment, request);
+ }
+
+ @TimedResource
+ @GET
+ @Path("/" + UPLOAD_PLUGIN_PAYMENT_STATE_MACHINE_CONFIG + "/{pluginName:" + ANYTHING_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Retrieve a per tenant payment state machine for a plugin", response = TenantKeyJson.class)
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid tenantId supplied")})
+ public Response getPluginPaymentStateMachineConfig(@PathParam("pluginName") final String pluginName,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TenantApiException {
+ return getTenantKey(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_, pluginName, request);
+ }
+
+ @TimedResource
+ @DELETE
+ @Path("/" + UPLOAD_PLUGIN_PAYMENT_STATE_MACHINE_CONFIG + "/{pluginName:" + ANYTHING_PATTERN + "}")
+ @ApiOperation(value = "Delete a per tenant payment state machine for a plugin")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid tenantId supplied")})
+ public Response deletePluginPaymentStateMachineConfig(@PathParam("pluginName") final String pluginName,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TenantApiException {
+ return deleteTenantKey(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_, pluginName, createdBy, reason, comment, request);
+ }
@TimedResource
@POST
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java
index a40529f..7a6b6aa 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java
@@ -20,6 +20,7 @@ import java.net.URI;
import java.util.Map;
import javax.annotation.Nullable;
+import javax.inject.Inject;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
@@ -27,19 +28,34 @@ import javax.ws.rs.core.UriInfo;
import org.killbill.billing.jaxrs.resources.JaxRsResourceBase;
import org.killbill.billing.jaxrs.resources.JaxrsResource;
+import org.killbill.billing.util.config.definition.JaxrsConfig;
public class JaxrsUriBuilder {
- public Response buildResponse(final UriInfo uriInfo, final Class<? extends JaxrsResource> theClass, final String getMethodName, final Object objectId) {
+ private final JaxrsConfig jaxrsConfig;
+
+ @Inject
+ public JaxrsUriBuilder(JaxrsConfig jaxrsConfig) {
+ this.jaxrsConfig = jaxrsConfig;
+ }
+
+ public Response buildResponse(final UriInfo uriInfo, final Class<? extends JaxrsResource> theClass,
+ final String getMethodName, final Object objectId) {
final URI location = buildLocation(uriInfo, theClass, getMethodName, objectId);
- return Response.created(location).build();
+ return !jaxrsConfig.isJaxrsLocationFullUrl() ?
+ Response.status(Response.Status.CREATED).header("Location", location.getPath()).build() :
+ Response.created(location).build();
}
- public URI buildLocation(final UriInfo uriInfo, final Class<? extends JaxrsResource> theClass, final String getMethodName, final Object objectId) {
- final UriBuilder uriBuilder = getUriBuilder(theClass, getMethodName).scheme(uriInfo.getAbsolutePath().getScheme())
- .host(uriInfo.getAbsolutePath().getHost())
- .port(uriInfo.getAbsolutePath().getPort());
+ public URI buildLocation(final UriInfo uriInfo, final Class<? extends JaxrsResource> theClass,
+ final String getMethodName, final Object objectId) {
+ final UriBuilder uriBuilder = getUriBuilder(uriInfo.getBaseUri().getPath(), theClass, getMethodName);
+ if (jaxrsConfig.isJaxrsLocationFullUrl()) {
+ uriBuilder.scheme(uriInfo.getAbsolutePath().getScheme())
+ .host(uriInfo.getAbsolutePath().getHost())
+ .port(uriInfo.getAbsolutePath().getPort());
+ }
return objectId != null ? uriBuilder.build(objectId) : uriBuilder.build();
}
@@ -74,6 +90,12 @@ public class JaxrsUriBuilder {
return ri.entity(obj).build();
}
+ private UriBuilder getUriBuilder(final String path, final Class<? extends JaxrsResource> theClassMaybeEnhanced, @Nullable final String getMethodName) {
+ final Class theClass = getNonEnhancedClass(theClassMaybeEnhanced);
+ return getMethodName != null ? UriBuilder.fromPath(path.equals("/") ? path.substring(1) : path).path(theClass).path(theClass, getMethodName) :
+ UriBuilder.fromPath(path).path(theClass);
+ }
+
private UriBuilder getUriBuilder(final Class<? extends JaxrsResource> theClassMaybeEnhanced, @Nullable final String getMethodName) {
final Class theClass = getNonEnhancedClass(theClassMaybeEnhanced);
return getMethodName != null ? UriBuilder.fromResource(theClass).path(theClass, getMethodName) :
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
index c27054c..b700827 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
@@ -73,6 +73,7 @@ public class TestBundleJsonWithSubscriptions extends JaxrsTestSuiteNoDB {
new LocalDate(),
new LocalDate(),
new LocalDate(),
+ null,
ImmutableList.<EventSubscriptionJson>of(event),
ImmutableList.of(priceOverride),
auditLogs);
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java
index 41ed2ba..529fc5a 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java
@@ -80,6 +80,7 @@ public class TestEntitlementJsonWithEvents extends JaxrsTestSuiteNoDB {
new LocalDate(),
new LocalDate(),
new LocalDate(),
+ null,
ImmutableList.<EventSubscriptionJson>of(newEvent),
ImmutableList.of(priceOverride),
null);
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
index b387188..7183e3c 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
@@ -81,7 +81,7 @@ public class DefaultBillingEvent implements BillingEvent {
final String prevPhaseName = transition.getPreviousPhase();
final PlanPhase prevPlanPhase = (prevPhaseName != null) ? catalog.findPhase(prevPhaseName, transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
- this.fixedPrice = getFixedPrice(nextPlanPhase, currency);
+ this.fixedPrice = transition.getTransitionType() != SubscriptionBaseTransitionType.BCD_CHANGE ? getFixedPrice(nextPlanPhase, currency) : null;
this.currency = currency;
this.description = transition.getTransitionType().toString();
this.billingPeriod = getRecurringBillingPeriod(isActive ? nextPlanPhase : prevPlanPhase);
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
index 7ec0b85..70cf640 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
@@ -31,8 +31,15 @@ import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.BillingAlignment;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.catalog.api.StaticCatalog;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.api.SubscriptionEventType;
@@ -48,6 +55,7 @@ import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
import org.killbill.billing.tag.TagInternalApi;
import org.killbill.billing.util.UUIDs;
+import org.killbill.billing.util.bcd.BillCycleDayCalculator;
import org.killbill.billing.util.tag.ControlTagType;
import org.killbill.billing.util.tag.Tag;
import org.killbill.clock.Clock;
@@ -128,7 +136,7 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
}
private void addBillingEventsForBundles(final List<SubscriptionBaseBundle> bundles, final ImmutableAccountData account, final DryRunArguments dryRunArguments, final InternalCallContext context,
- final DefaultBillingEventSet result, final Set<UUID> skipSubscriptionsSet) throws SubscriptionBaseApiException, AccountApiException {
+ final DefaultBillingEventSet result, final Set<UUID> skipSubscriptionsSet) throws AccountApiException, CatalogApiException, SubscriptionBaseApiException {
final boolean dryRunMode = dryRunArguments != null;
@@ -140,7 +148,7 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
final UUID fakeBundleId = UUIDs.randomUUID();
final List<SubscriptionBase> subscriptions = subscriptionApi.getSubscriptionsForBundle(fakeBundleId, dryRunArguments, context);
- addBillingEventsForSubscription(account, subscriptions, fakeBundleId, dryRunMode, context, result, skipSubscriptionsSet);
+ addBillingEventsForSubscription(account, subscriptions, null, dryRunMode, context, result, skipSubscriptionsSet);
}
@@ -159,7 +167,8 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
result.getSubscriptionIdsWithAutoInvoiceOff().add(subscription.getId());
}
} else { // billing is not off
- addBillingEventsForSubscription(account, subscriptions, bundle.getId(), dryRunMode, context, result, skipSubscriptionsSet);
+ final SubscriptionBase baseSubscription = !subscriptions.isEmpty() ? subscriptions.get(0) : null;
+ addBillingEventsForSubscription(account, subscriptions, baseSubscription, dryRunMode, context, result, skipSubscriptionsSet);
}
}
}
@@ -167,11 +176,11 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
private void addBillingEventsForSubscription(final ImmutableAccountData account,
final List<SubscriptionBase> subscriptions,
- final UUID bundleId,
+ final SubscriptionBase baseSubscription,
final boolean dryRunMode,
final InternalCallContext context,
final DefaultBillingEventSet result,
- final Set<UUID> skipSubscriptionsSet) throws AccountApiException {
+ final Set<UUID> skipSubscriptionsSet) throws AccountApiException, CatalogApiException, SubscriptionBaseApiException {
// If dryRun is specified, we don't want to to update the account BCD value, so we initialize the flag updatedAccountBCD to true
boolean updatedAccountBCD = dryRunMode;
@@ -194,31 +203,58 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
return;
}
+ final Catalog catalog = catalogService.getFullCatalog(context);
+ Integer overridenBCD = null;
for (final EffectiveSubscriptionInternalEvent transition : billingTransitions) {
- try {
- final int bcdLocal = bcdCalculator.calculateBcd(account, currentAccountBCD, bundleId, subscription, transition, context);
-
- if (currentAccountBCD == 0 && !updatedAccountBCD) {
- accountApi.updateBCD(account.getExternalKey(), bcdLocal, context);
- updatedAccountBCD = true;
- }
-
- final BillingEvent event = new DefaultBillingEvent(account, transition, subscription, bcdLocal, account.getCurrency(), catalogService.getFullCatalog(context));
- result.add(event);
- } catch (CatalogApiException e) {
- log.error("Failing to identify catalog components while creating BillingEvent from transition: " +
- transition.getId().toString(), e);
- } catch (AccountApiException e) {
- // This is unexpected (failed to update BCD) but if this happens we don't want to ignore..
- throw e;
- } catch (Exception e) {
- log.warn("Failed while getting BillingEvent", e);
+ //
+ // A BCD_CHANGE transition defines a new billCycleDayLocal for the subscription and this overrides whatever computation
+ // occurs below (which is based on billing alignment policy). Also multiple of those BCD_CHANGE transitions could occur,
+ // to define different intervals with different billing cycle days.
+ //
+ overridenBCD = transition.getNextBillCycleDayLocal() != null ? transition.getNextBillCycleDayLocal() : overridenBCD;
+ final int bcdLocal = overridenBCD != null ?
+ overridenBCD :
+ calculateBcdForTransition(catalog, baseSubscription, subscription, account, currentAccountBCD, transition);
+
+ if (currentAccountBCD == 0 && !updatedAccountBCD) {
+ accountApi.updateBCD(account.getExternalKey(), bcdLocal, context);
+ updatedAccountBCD = true;
}
+
+ final BillingEvent event = new DefaultBillingEvent(account, transition, subscription, bcdLocal, account.getCurrency(), catalog);
+ result.add(event);
}
}
}
+ private int calculateBcdForTransition(final Catalog catalog, final SubscriptionBase baseSubscription, final SubscriptionBase subscription, final ImmutableAccountData account, final int accountBillCycleDayLocal, final EffectiveSubscriptionInternalEvent transition)
+ throws CatalogApiException, AccountApiException, SubscriptionBaseApiException {
+ final BillingAlignment alignment = catalog.billingAlignment(getPlanPhaseSpecifierFromTransition(catalog, transition), transition.getEffectiveTransitionTime());
+ return bcdCalculator.calculateBcdForAlignment(subscription, baseSubscription, alignment, account.getTimeZone(), accountBillCycleDayLocal);
+ }
+
+ private PlanPhaseSpecifier getPlanPhaseSpecifierFromTransition(final Catalog catalog, final EffectiveSubscriptionInternalEvent transition) throws CatalogApiException {
+ final Plan prevPlan = (transition.getPreviousPlan() != null) ? catalog.findPlan(transition.getPreviousPlan(), transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
+ final Plan nextPlan = (transition.getNextPlan() != null) ? catalog.findPlan(transition.getNextPlan(), transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
+
+ final Plan plan = (transition.getTransitionType() != SubscriptionBaseTransitionType.CANCEL) ? nextPlan : prevPlan;
+ final Product product = plan.getProduct();
+
+ final PlanPhase prevPhase = (transition.getPreviousPhase() != null) ? catalog.findPhase(transition.getPreviousPhase(), transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
+ final PlanPhase nextPhase = (transition.getNextPhase() != null) ? catalog.findPhase(transition.getNextPhase(), transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
+
+ final PlanPhase phase = (transition.getTransitionType() != SubscriptionBaseTransitionType.CANCEL) ? nextPhase : prevPhase;
+
+ final BillingPeriod billingPeriod = phase.getRecurring() != null ? phase.getRecurring().getBillingPeriod() : BillingPeriod.NO_BILLING_PERIOD;
+
+ return new PlanPhaseSpecifier(product.getName(),
+ product.getCategory(),
+ billingPeriod,
+ transition.getNextPriceList(),
+ phase.getPhaseType());
+ }
+
private final boolean is_AUTO_INVOICING_OFF(final List<Tag> tags) {
return ControlTagType.isAutoInvoicingOff(Collections2.transform(tags, new Function<Tag, UUID>() {
@Nullable
diff --git a/junction/src/test/java/org/killbill/billing/junction/JunctionTestSuiteNoDB.java b/junction/src/test/java/org/killbill/billing/junction/JunctionTestSuiteNoDB.java
index f13d01e..4a84424 100644
--- a/junction/src/test/java/org/killbill/billing/junction/JunctionTestSuiteNoDB.java
+++ b/junction/src/test/java/org/killbill/billing/junction/JunctionTestSuiteNoDB.java
@@ -16,21 +16,19 @@
package org.killbill.billing.junction;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.BeforeMethod;
-
import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
-import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.catalog.api.CatalogService;
import org.killbill.billing.entitlement.dao.BlockingStateDao;
import org.killbill.billing.junction.glue.TestJunctionModuleNoDB;
-import org.killbill.billing.junction.plumbing.billing.BillCycleDayCalculator;
import org.killbill.billing.junction.plumbing.billing.BlockingCalculator;
-import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
import org.killbill.billing.tag.TagInternalApi;
import org.killbill.billing.util.tag.dao.TagDao;
+import org.killbill.bus.api.PersistentBus;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
import com.google.inject.Guice;
import com.google.inject.Inject;
@@ -41,8 +39,6 @@ public abstract class JunctionTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
@Inject
protected AccountInternalApi accountInternalApi;
@Inject
- protected BillCycleDayCalculator billCycleDayCalculator;
- @Inject
protected BillingInternalApi billingInternalApi;
@Inject
protected BlockingCalculator blockingCalculator;
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
index df07cbc..2fc7a4b 100644
--- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
@@ -92,7 +92,7 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
effectiveSubscriptionTransitions = new LinkedList<EffectiveSubscriptionInternalEvent>();
final DateTime subscriptionStartDate = clock.getUTCNow().minusDays(3);
- subscription = new MockSubscription(subId, bunId, null, subscriptionStartDate, effectiveSubscriptionTransitions);
+ subscription = new MockSubscription(subId, bunId, null, subscriptionStartDate, subscriptionStartDate);
final List<SubscriptionBase> subscriptions = ImmutableList.<SubscriptionBase>of(subscription);
Mockito.when(subscriptionInternalApi.getBundlesForAccount(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(bundles);
@@ -281,9 +281,9 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
final PriceList nextPriceList = catalog.findPriceList(PriceListSet.DEFAULT_PRICELIST_NAME, now);
final EffectiveSubscriptionInternalEvent t = new MockEffectiveSubscriptionEvent(
- eventId, subId, bunId, then, now, null, null, null, null, EntitlementState.ACTIVE,
+ eventId, subId, bunId, then, now, null, null, null, null, null, EntitlementState.ACTIVE,
nextPlan.getName(), nextPhase.getName(),
- nextPriceList.getName(), 1L,
+ nextPriceList.getName(), null, 1L,
SubscriptionBaseTransitionType.CREATE, 1, null, 1L, 2L, null);
effectiveSubscriptionTransitions.add(t);
NEWS 11(+10 -1)
diff --git a/NEWS b/NEWS
index ad512cc..842a436 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,9 @@
+0.17.0
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.17.0
+
+0.16.6
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.16.6
+
0.16.5
See https://github.com/killbill/killbill/releases/tag/killbill-0.16.5
@@ -14,7 +20,7 @@
See https://github.com/killbill/killbill/releases/tag/killbill-0.16.1
0.16.0
- TBD (point to 0.16 release page)
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.16.0
0.15.10
See https://github.com/killbill/killbill/releases/tag/killbill-0.15.10
@@ -44,6 +50,9 @@
0.15.0
See https://github.com/killbill/killbill/issues?q=milestone%3ARelease-0.15.0+is%3Aclosed
+0.14.1
+ Fix usage bug (see 8511f41cdf78bd1cb9d0c335ffa8d40ca53aeccd)
+
0.14.0
http://killbill.io/blog/kill-bill-0-14-0-released/
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java
index a256c61..9058564 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * 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
@@ -17,43 +17,58 @@
package org.killbill.billing.payment.api;
-import java.util.UUID;
-
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.payment.core.PaymentTransactionInfoPluginConverter;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
-import org.killbill.commons.locker.GlobalLocker;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class DefaultAdminPaymentApi implements AdminPaymentApi {
+import org.killbill.billing.util.config.definition.PaymentConfig;
- private static final Logger log = LoggerFactory.getLogger(DefaultAdminPaymentApi.class);
+public class DefaultAdminPaymentApi extends DefaultApiBase implements AdminPaymentApi {
private final PaymentDao paymentDao;
- private final GlobalLocker locker;
private final InternalCallContextFactory internalCallContextFactory;
@Inject
- public DefaultAdminPaymentApi(final PaymentDao paymentDao, final InternalCallContextFactory internalCallContextFactory, final GlobalLocker locker) {
+ public DefaultAdminPaymentApi(final PaymentConfig paymentConfig, final PaymentDao paymentDao, final InternalCallContextFactory internalCallContextFactory) {
+ super(paymentConfig, internalCallContextFactory);
this.paymentDao = paymentDao;
this.internalCallContextFactory = internalCallContextFactory;
- this.locker = locker;
}
@Override
- public void fixPaymentTransactionState(final Payment payment, PaymentTransaction paymentTransaction, TransactionStatus transactionStatus, @Nullable String lastSuccessPaymentState, String currentPaymentStateName,
- Iterable<PluginProperty> properties, CallContext callContext)
- throws PaymentApiException {
-
+ public void fixPaymentTransactionState(final Payment payment,
+ final PaymentTransaction paymentTransaction,
+ @Nullable final TransactionStatus transactionStatusMaybeNull,
+ @Nullable final String lastSuccessPaymentState,
+ final String currentPaymentStateName,
+ final Iterable<PluginProperty> properties,
+ final CallContext callContext) throws PaymentApiException {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(payment.getAccountId(), callContext);
- paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), payment.getId(), paymentTransaction.getTransactionType(),
- currentPaymentStateName, lastSuccessPaymentState, paymentTransaction.getId(),
- transactionStatus, paymentTransaction.getProcessedAmount(), paymentTransaction.getProcessedCurrency(),
- paymentTransaction.getGatewayErrorCode(), paymentTransaction.getGatewayErrorMsg(), internalCallContext);
+
+ final TransactionStatus transactionStatus;
+ if (transactionStatusMaybeNull == null) {
+ checkNotNullParameter(paymentTransaction.getPaymentInfoPlugin(), "PaymentTransactionInfoPlugin");
+ transactionStatus = PaymentTransactionInfoPluginConverter.toTransactionStatus(paymentTransaction.getPaymentInfoPlugin());
+ } else {
+ transactionStatus = transactionStatusMaybeNull;
+ }
+
+ paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(),
+ null,
+ payment.getId(),
+ paymentTransaction.getTransactionType(),
+ currentPaymentStateName,
+ lastSuccessPaymentState,
+ paymentTransaction.getId(),
+ transactionStatus,
+ paymentTransaction.getProcessedAmount(),
+ paymentTransaction.getProcessedCurrency(),
+ paymentTransaction.getGatewayErrorCode(),
+ paymentTransaction.getGatewayErrorMsg(),
+ internalCallContext);
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
index 33b8730..562889a 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
@@ -29,8 +29,12 @@ import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+import com.google.common.base.Function;
import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -80,13 +84,77 @@ public class DefaultPayment extends EntityBase implements Payment {
}
}
- final BigDecimal chargebackAmount = getChargebackAmount(transactions);
-
- this.authAmount = getAmountForType(nonVoidedTransactions, TransactionType.AUTHORIZE);
- this.captureAmount = getAmountForType(nonVoidedTransactions, TransactionType.CAPTURE).add(chargebackAmount.negate()).max(BigDecimal.ZERO);
- this.purchasedAmount = getAmountForType(nonVoidedTransactions, TransactionType.PURCHASE).add(chargebackAmount.negate()).max(BigDecimal.ZERO);
- this.creditAmount = getAmountForType(nonVoidedTransactions, TransactionType.CREDIT);
- this.refundAmount = getAmountForType(nonVoidedTransactions, TransactionType.REFUND);
+ final Collection<PaymentTransaction> chargebackTransactions = getChargebackTransactions(transactions);
+ final Currency chargebackProcessedCurrency = getCurrencyForTransactions(chargebackTransactions, true);
+ final BigDecimal chargebackProcessedAmount = chargebackProcessedCurrency == null ? BigDecimal.ZERO : getAmountForTransactions(chargebackTransactions, true);
+ final Currency chargebackCurrency = getCurrencyForTransactions(chargebackTransactions, false);
+ final BigDecimal chargebackAmount = chargebackCurrency == null ? BigDecimal.ZERO : getAmountForTransactions(chargebackTransactions, false);
+
+ PaymentTransaction transactionToUseForCurrency = Iterables.<PaymentTransaction>getFirst(Iterables.<PaymentTransaction>filter(transactions,
+ new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction transaction) {
+ return (transaction.getTransactionType() == TransactionType.AUTHORIZE ||
+ transaction.getTransactionType() == TransactionType.PURCHASE ||
+ transaction.getTransactionType() == TransactionType.CREDIT) &&
+ (TransactionStatus.SUCCESS.equals(transaction.getTransactionStatus()) ||
+ TransactionStatus.PENDING.equals(transaction.getTransactionStatus()));
+ }
+ }), null);
+ if (transactionToUseForCurrency == null) {
+ // No successful one, take the last non-successful one then
+ transactionToUseForCurrency = Iterables.<PaymentTransaction>getLast(Iterables.<PaymentTransaction>filter(transactions,
+ new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction transaction) {
+ return transaction.getTransactionType() == TransactionType.AUTHORIZE ||
+ transaction.getTransactionType() == TransactionType.PURCHASE ||
+ transaction.getTransactionType() == TransactionType.CREDIT;
+ }
+ }), null);
+ }
+ this.currency = transactionToUseForCurrency == null ? null : transactionToUseForCurrency.getCurrency();
+
+ this.authAmount = getAmountForTransactions(this.currency,
+ nonVoidedTransactions,
+ TransactionType.AUTHORIZE,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
+ this.captureAmount = getAmountForTransactions(this.currency,
+ nonVoidedTransactions,
+ TransactionType.CAPTURE,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
+ this.purchasedAmount = getAmountForTransactions(this.currency,
+ nonVoidedTransactions,
+ TransactionType.PURCHASE,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
+ this.creditAmount = getAmountForTransactions(this.currency,
+ nonVoidedTransactions,
+ TransactionType.CREDIT,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
+ this.refundAmount = getAmountForTransactions(this.currency,
+ nonVoidedTransactions,
+ TransactionType.REFUND,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
this.isAuthVoided = Iterables.<PaymentTransaction>tryFind(voidedTransactions,
new Predicate<PaymentTransaction>() {
@@ -95,11 +163,9 @@ public class DefaultPayment extends EntityBase implements Payment {
return input.getTransactionType() == TransactionType.AUTHORIZE && TransactionStatus.SUCCESS.equals(input.getTransactionStatus());
}
}).isPresent();
-
- this.currency = !transactions.isEmpty() ? transactions.get(0).getCurrency() : null;
}
- private static BigDecimal getChargebackAmount(final Iterable<PaymentTransaction> transactions) {
+ private static Collection<PaymentTransaction> getChargebackTransactions(final Collection<PaymentTransaction> transactions) {
final Collection<String> successfulChargebackExternalKeys = new HashSet<String>();
for (final PaymentTransaction transaction : transactions) {
@@ -111,37 +177,116 @@ public class DefaultPayment extends EntityBase implements Payment {
}
}
- return getAmountForType(Iterables.<PaymentTransaction>filter(transactions, new Predicate<PaymentTransaction>() {
- @Override
- public boolean apply(final PaymentTransaction input) {
- return successfulChargebackExternalKeys.contains(input.getExternalKey());
- }
- }),
- TransactionType.CHARGEBACK);
+ return Collections2.<PaymentTransaction>filter(transactions, new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction input) {
+ return successfulChargebackExternalKeys.contains(input.getExternalKey());
+ }
+ });
}
- private static BigDecimal getAmountForType(final Iterable<PaymentTransaction> transactions, final TransactionType transactiontype) {
- BigDecimal result = BigDecimal.ZERO;
- BigDecimal processedResult = BigDecimal.ZERO;
- boolean shouldUseProcessedAmount = true;
-
- for (final PaymentTransaction transaction : transactions) {
- if (transaction.getTransactionType() != transactiontype || !TransactionStatus.SUCCESS.equals(transaction.getTransactionStatus())) {
- continue;
+ private static BigDecimal getAmountForTransactions(final Currency paymentCurrency,
+ final Collection<PaymentTransaction> transactions,
+ final TransactionType transactiontype,
+ final Collection<PaymentTransaction> chargebackTransactions,
+ final BigDecimal chargebackProcessedAmount,
+ final Currency chargebackProcessedCurrency,
+ final BigDecimal chargebackAmount,
+ final Currency chargebackCurrency) {
+ BigDecimal unformattedAmountForTransactions = null;
+
+ final Collection<PaymentTransaction> candidateTransactions = Collections2.<PaymentTransaction>filter(transactions,
+ new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction transaction) {
+ return transaction.getTransactionType() == transactiontype && TransactionStatus.SUCCESS.equals(transaction.getTransactionStatus());
+ }
+ });
+
+ final boolean takeChargebacksIntoAccount = ImmutableList.<TransactionType>of(TransactionType.CAPTURE, TransactionType.PURCHASE).contains(transactiontype);
+ Currency currencyForTransactions = getCurrencyForTransactions(candidateTransactions, true);
+ if (currencyForTransactions == null || currencyForTransactions != paymentCurrency) {
+ currencyForTransactions = getCurrencyForTransactions(candidateTransactions, false);
+ if (currencyForTransactions == null) {
+ // Multiple currencies - cannot compute the total
+ unformattedAmountForTransactions = BigDecimal.ZERO;
+ } else if (currencyForTransactions != paymentCurrency) {
+ // Different currency than the main payment currency
+ unformattedAmountForTransactions = BigDecimal.ZERO;
+ } else {
+ final BigDecimal amountForTransactions = getAmountForTransactions(candidateTransactions, false);
+ unformattedAmountForTransactions = getAmountForTransactions(amountForTransactions,
+ takeChargebacksIntoAccount,
+ currencyForTransactions,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
}
+ } else {
+ final BigDecimal amountForTransactions = getAmountForTransactions(candidateTransactions, true);
+ unformattedAmountForTransactions = getAmountForTransactions(amountForTransactions,
+ takeChargebacksIntoAccount,
+ currencyForTransactions,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
+ }
- result = result.add(transaction.getAmount());
+ return unformattedAmountForTransactions == null || currencyForTransactions == null ? unformattedAmountForTransactions : KillBillMoney.of(unformattedAmountForTransactions, currencyForTransactions);
+ }
- shouldUseProcessedAmount = shouldUseProcessedAmount && transaction.getCurrency().equals(transaction.getProcessedCurrency()) && transaction.getProcessedAmount() != null;
- processedResult = shouldUseProcessedAmount ? processedResult.add(transaction.getProcessedAmount()) : BigDecimal.ZERO;
+ private static BigDecimal getAmountForTransactions(final BigDecimal amountForTransactions,
+ final boolean takeChargebacksIntoAccount,
+ final Currency currencyForTransactions,
+ final Collection<PaymentTransaction> chargebackTransactions,
+ final BigDecimal chargebackProcessedAmount,
+ final Currency chargebackProcessedCurrency,
+ final BigDecimal chargebackAmount,
+ final Currency chargebackCurrency) {
+ if (!takeChargebacksIntoAccount) {
+ return amountForTransactions;
+ }
- // For multi-step AUTH, don't sum the individual transactions
- if (TransactionType.AUTHORIZE.equals(transactiontype)) {
- break;
+ final BigDecimal chargebackAmountInCorrectCurrency;
+ if (currencyForTransactions == chargebackProcessedCurrency) {
+ chargebackAmountInCorrectCurrency = chargebackProcessedAmount;
+ } else if (currencyForTransactions == chargebackCurrency) {
+ chargebackAmountInCorrectCurrency = chargebackAmount;
+ } else if (!chargebackTransactions.isEmpty()) {
+ // Payment has chargebacks but in a different currency - zero-out the payment
+ chargebackAmountInCorrectCurrency = amountForTransactions;
+ } else {
+ chargebackAmountInCorrectCurrency = BigDecimal.ZERO;
+ }
+ return amountForTransactions.add(chargebackAmountInCorrectCurrency.negate()).max(BigDecimal.ZERO);
+ }
+
+ private static BigDecimal getAmountForTransactions(final Iterable<PaymentTransaction> candidateTransactions, final boolean useProcessedValues) {
+ BigDecimal amount = BigDecimal.ZERO;
+ for (final PaymentTransaction transaction : candidateTransactions) {
+ if (useProcessedValues) {
+ amount = amount.add(transaction.getProcessedAmount());
+ } else {
+ amount = amount.add(transaction.getAmount());
}
}
+ return amount;
+ }
+
+ private static Currency getCurrencyForTransactions(final Collection<PaymentTransaction> candidateTransactions, final boolean useProcessedValues) {
+ final Collection<Currency> currencies = new HashSet<Currency>(Collections2.<PaymentTransaction, Currency>transform(candidateTransactions,
+ new Function<PaymentTransaction, Currency>() {
+ @Override
+ public Currency apply(final PaymentTransaction transaction) {
+ return useProcessedValues ? transaction.getProcessedCurrency() : transaction.getCurrency();
+ }
+ }));
- return shouldUseProcessedAmount ? processedResult : result;
+ return currencies.size() > 1 ? null : Iterables.<Currency>getFirst(currencies, null);
}
@Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
index e0751bd..5bfd18d 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
@@ -89,7 +89,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = paymentProcessor.createAuthorization(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -133,7 +135,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = pluginControlPaymentProcessor.createAuthorization(IS_API_PAYMENT, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -170,7 +174,8 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
payment = paymentProcessor.createCapture(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, amount, currency, paymentTransactionExternalKey,
SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -210,7 +215,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = pluginControlPaymentProcessor.createCapture(IS_API_PAYMENT, account, paymentId, amount, currency, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -249,7 +256,8 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
payment = paymentProcessor.createPurchase(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -299,7 +307,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
payment = pluginControlPaymentProcessor.createPurchase(IS_API_PAYMENT, account, nonNulPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -333,7 +343,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = paymentProcessor.createVoid(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, paymentTransactionExternalKey,
SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -371,7 +383,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = pluginControlPaymentProcessor.createVoid(IS_API_PAYMENT, account, paymentId, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -407,7 +421,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = paymentProcessor.createRefund(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, amount, currency, paymentTransactionExternalKey,
SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -449,7 +465,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = pluginControlPaymentProcessor.createRefund(IS_API_PAYMENT, account, paymentId, amount, currency, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -488,7 +506,8 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
payment = paymentProcessor.createCredit(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -531,7 +550,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = pluginControlPaymentProcessor.createCredit(IS_API_PAYMENT, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -606,7 +627,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = paymentProcessor.createChargeback(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, paymentTransactionExternalKey, amount, currency, true,
callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -645,7 +668,8 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
payment = pluginControlPaymentProcessor.createChargeback(IS_API_PAYMENT, account, paymentId, paymentTransactionExternalKey, amount, currency,
paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -677,14 +701,7 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = paymentProcessor.createChargebackReversal(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, paymentTransactionExternalKey, null, null, true, callContext, internalCallContext);
- // See https://github.com/killbill/killbill/issues/552
- paymentTransaction = Iterables.<PaymentTransaction>find(Lists.<PaymentTransaction>reverse(payment.getTransactions()),
- new Predicate<PaymentTransaction>() {
- @Override
- public boolean apply(final PaymentTransaction input) {
- return paymentTransactionExternalKey.equals(input.getExternalKey());
- }
- });
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
return payment;
} finally {
@@ -869,6 +886,21 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
return paymentMethods;
}
+ private PaymentTransaction findPaymentTransaction(final Payment payment, @Nullable final String paymentTransactionExternalKey) {
+ // By design, the payment transactions are already correctly sorted (by effective date asc)
+ if (paymentTransactionExternalKey == null) {
+ return Iterables.getLast(payment.getTransactions());
+ } else {
+ return Iterables.<PaymentTransaction>find(Lists.<PaymentTransaction>reverse(payment.getTransactions()),
+ new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction input) {
+ return paymentTransactionExternalKey.equals(input.getExternalKey());
+ }
+ });
+ }
+ }
+
private void logEnterAPICall(final String transactionType,
final Account account,
@Nullable final UUID paymentMethodId,
@@ -919,8 +951,6 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
paymentControlPluginNames);
}
-
-
private void logAPICallInternal(final String prefixMsg,
final String transactionType,
final Account account,
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentTransaction.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentTransaction.java
index 74710f0..7e28bdb 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentTransaction.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentTransaction.java
@@ -1,5 +1,6 @@
/*
- * Copyright 2014 Groupon, Inc
+ * 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
@@ -23,6 +24,7 @@ import org.joda.time.DateTime;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.entity.EntityBase;
import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin;
+import org.killbill.billing.util.currency.KillBillMoney;
public class DefaultPaymentTransaction extends EntityBase implements PaymentTransaction {
@@ -50,9 +52,9 @@ public class DefaultPaymentTransaction extends EntityBase implements PaymentTran
this.transactionType = transactionType;
this.effectiveDate = effectiveDate;
this.status = status;
- this.amount = amount;
+ this.amount = amount == null || currency == null ? amount : KillBillMoney.of(amount, currency);
this.currency = currency;
- this.processedAmount = processedAmount;
+ this.processedAmount = processedAmount == null || processedCurrency == null ? processedAmount : KillBillMoney.of(processedAmount, processedCurrency);
this.processedCurrency = processedCurrency;
this.gatewayErrorCode = gatewayErrorCode;
this.gatewayErrorMsg = gatewayErrorMsg;
diff --git a/payment/src/main/java/org/killbill/billing/payment/bus/PaymentBusEventHandler.java b/payment/src/main/java/org/killbill/billing/payment/bus/PaymentBusEventHandler.java
index 1c1d8dc..145507a 100644
--- a/payment/src/main/java/org/killbill/billing/payment/bus/PaymentBusEventHandler.java
+++ b/payment/src/main/java/org/killbill/billing/payment/bus/PaymentBusEventHandler.java
@@ -101,10 +101,9 @@ public class PaymentBusEventHandler {
} catch (final AccountApiException e) {
log.warn("Failed to process invoice payment", e);
} catch (final PaymentApiException e) {
- // Log as error unless:
- if (e.getCode() != ErrorCode.PAYMENT_NULL_INVOICE.getCode() /* Nothing left to be paid */ &&
- e.getCode() != ErrorCode.PAYMENT_CREATE_PAYMENT.getCode() /* User payment error */) {
- log.error("Failed to process invoice payment {}", e.toString());
+ // Log as warn unless nothing left to be paid
+ if (e.getCode() != ErrorCode.PAYMENT_PLUGIN_API_ABORTED.getCode()) {
+ log.warn("Failed to process invoice payment {}", e.toString());
}
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java b/payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java
new file mode 100644
index 0000000..ae76cb2
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 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.payment.caching;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URI;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.killbill.automaton.DefaultStateMachineConfig;
+import org.killbill.automaton.StateMachineConfig;
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.glue.PaymentModule;
+import org.killbill.billing.tenant.api.TenantInternalApi;
+import org.killbill.billing.tenant.api.TenantInternalApi.CacheInvalidationCallback;
+import org.killbill.billing.tenant.api.TenantKV.TenantKey;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.cache.CacheLoaderArgument;
+import org.killbill.billing.util.cache.TenantStateMachineConfigCacheLoader.LoaderCallback;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.xmlloader.XMLLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.io.Resources;
+
+public class EhCacheStateMachineConfigCache implements StateMachineConfigCache {
+
+ private static final Logger logger = LoggerFactory.getLogger(EhCacheStateMachineConfigCache.class);
+
+ private final TenantInternalApi tenantInternalApi;
+ private final CacheController cacheController;
+ private final CacheInvalidationCallback cacheInvalidationCallback;
+ private final LoaderCallback loaderCallback;
+
+ private StateMachineConfig defaultPaymentStateMachineConfig;
+
+ @Inject
+ public EhCacheStateMachineConfigCache(final TenantInternalApi tenantInternalApi,
+ final CacheControllerDispatcher cacheControllerDispatcher,
+ @Named(PaymentModule.STATE_MACHINE_CONFIG_INVALIDATION_CALLBACK) final CacheInvalidationCallback cacheInvalidationCallback) {
+ this.tenantInternalApi = tenantInternalApi;
+ // Can be null if mis-configured (e.g. missing in ehcache.xml)
+ this.cacheController = cacheControllerDispatcher.getCacheController(CacheType.TENANT_PAYMENT_STATE_MACHINE_CONFIG);
+ this.cacheInvalidationCallback = cacheInvalidationCallback;
+ this.loaderCallback = new LoaderCallback() {
+ public Object loadStateMachineConfig(final String stateMachineConfigXML) throws PaymentApiException {
+ tenantInternalApi.initializeCacheInvalidationCallback(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_, cacheInvalidationCallback);
+
+ try {
+ final InputStream stream = new ByteArrayInputStream(stateMachineConfigXML.getBytes());
+ return XMLLoader.getObjectFromStream(new URI("dummy"), stream, DefaultStateMachineConfig.class);
+ } catch (final Exception e) {
+ // TODO 0.17 proper error code
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, "Invalid payment state machine config");
+ }
+ }
+ };
+ }
+
+ @Override
+ public void loadDefaultPaymentStateMachineConfig(final String url) throws PaymentApiException {
+ if (url != null) {
+ try {
+ defaultPaymentStateMachineConfig = XMLLoader.getObjectFromString(Resources.getResource(url).toExternalForm(), DefaultStateMachineConfig.class);
+ } catch (final Exception e) {
+ // TODO 0.17 proper error code
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, "Invalid default payment state machine config");
+ }
+ }
+ }
+
+ @Override
+ public StateMachineConfig getPaymentStateMachineConfig(final String pluginName, final InternalTenantContext tenantContext) throws PaymentApiException {
+ if (tenantContext.getTenantRecordId() == InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID || cacheController == null) {
+ return defaultPaymentStateMachineConfig;
+ }
+
+ final String pluginConfigKey = getCacheKeyName(pluginName, tenantContext);
+ final CacheLoaderArgument cacheLoaderArgument = createCacheLoaderArgument(pluginName);
+ try {
+ StateMachineConfig pluginPaymentStateMachineConfig = (StateMachineConfig) cacheController.get(pluginConfigKey, cacheLoaderArgument);
+ // It means we are using the default state machine config in a multi-tenant deployment
+ if (pluginPaymentStateMachineConfig == null) {
+ pluginPaymentStateMachineConfig = defaultPaymentStateMachineConfig;
+ cacheController.add(pluginConfigKey, pluginPaymentStateMachineConfig);
+ }
+ return pluginPaymentStateMachineConfig;
+ } catch (final IllegalStateException e) {
+ // TODO 0.17 proper error code
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, "Invalid payment state machine");
+ }
+ }
+
+ // See also DefaultTenantUserApi - we use the same conventions as the main XML cache (so we can re-use the invalidation code)
+ private String getCacheKeyName(final String pluginName, final InternalTenantContext internalContext) {
+ final StringBuilder tenantKey = new StringBuilder(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString());
+ tenantKey.append(pluginName);
+ tenantKey.append(CacheControllerDispatcher.CACHE_KEY_SEPARATOR);
+ tenantKey.append(internalContext.getTenantRecordId());
+ return tenantKey.toString();
+ }
+
+ @Override
+ public void clearPaymentStateMachineConfig(final String pluginName, final InternalTenantContext tenantContext) {
+ if (tenantContext.getTenantRecordId() != InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID && cacheController != null) {
+ final String key = getCacheKeyName(pluginName, tenantContext);
+ cacheController.remove(key);
+ }
+ }
+
+ private CacheLoaderArgument createCacheLoaderArgument(final String pluginName) {
+ final Object[] args = new Object[2];
+ args[0] = loaderCallback;
+ args[1] = pluginName;
+ final ObjectType irrelevant = null;
+ final InternalTenantContext notUsed = null;
+ return new CacheLoaderArgument(irrelevant, args, notUsed);
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCache.java b/payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCache.java
new file mode 100644
index 0000000..64c77ad
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCache.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 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.payment.caching;
+
+import org.killbill.automaton.StateMachineConfig;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.payment.api.PaymentApiException;
+
+public interface StateMachineConfigCache {
+
+ public void loadDefaultPaymentStateMachineConfig(String url) throws PaymentApiException;
+
+ public StateMachineConfig getPaymentStateMachineConfig(String pluginName, InternalTenantContext tenantContext) throws PaymentApiException;
+
+ public void clearPaymentStateMachineConfig(String pluginName, InternalTenantContext tenantContext);
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCacheInvalidationCallback.java b/payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCacheInvalidationCallback.java
new file mode 100644
index 0000000..4a47495
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCacheInvalidationCallback.java
@@ -0,0 +1,49 @@
+/*
+ * 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.payment.caching;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.tenant.api.TenantInternalApi.CacheInvalidationCallback;
+import org.killbill.billing.tenant.api.TenantKV.TenantKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+// Similar to TenantCacheInvalidationCallback
+public class StateMachineConfigCacheInvalidationCallback implements CacheInvalidationCallback {
+
+ private final Logger log = LoggerFactory.getLogger(StateMachineConfigCacheInvalidationCallback.class);
+
+ private final StateMachineConfigCache stateMachineConfigCache;
+
+ @Inject
+ public StateMachineConfigCacheInvalidationCallback(final StateMachineConfigCache stateMachineConfigCache) {
+ this.stateMachineConfigCache = stateMachineConfigCache;
+ }
+
+ @Override
+ public void invalidateCache(final TenantKey tenantKey, final Object cookie, final InternalTenantContext tenantContext) {
+ if (cookie == null) {
+ return;
+ }
+
+ log.info("Invalidate payment state machine config cache for pluginName='{}', tenantRecordId='{}'", cookie, tenantContext.getTenantRecordId());
+ stateMachineConfigCache.clearPaymentStateMachineConfig(cookie.toString(), tenantContext);
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
index 5c6d3d8..5fd6dd2 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
@@ -241,7 +241,7 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
payment.getId(), paymentTransaction.getId(), paymentTransaction.getTransactionStatus(), transactionStatus);
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(payment.getAccountId(), callContext);
- paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), payment.getId(), paymentTransaction.getTransactionType(), newPaymentState, lastSuccessPaymentState,
+ paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), paymentTransaction.getAttemptId(), payment.getId(), paymentTransaction.getTransactionType(), newPaymentState, lastSuccessPaymentState,
paymentTransaction.getId(), transactionStatus, processedAmount, processedCurrency, gatewayErrorCode, gatewayError, internalCallContext);
return true;
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 e4d9edf..4df2541 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
@@ -49,7 +49,9 @@ import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.api.TransactionType;
import org.killbill.billing.payment.core.janitor.IncompletePaymentTransactionTask;
+import org.killbill.billing.payment.core.sm.PaymentAutomatonDAOHelper;
import org.killbill.billing.payment.core.sm.PaymentAutomatonRunner;
+import org.killbill.billing.payment.core.sm.PaymentStateContext;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.dao.PaymentModelDao;
import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
@@ -70,6 +72,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
@@ -169,30 +172,33 @@ public class PaymentProcessor extends ProcessorBase {
final Map<UUID, PaymentPluginApi> paymentPluginByPaymentMethodId = new HashMap<UUID, PaymentPluginApi>();
final Collection<UUID> absentPlugins = new HashSet<UUID>();
- return Lists.<PaymentModelDao, Payment>transform(paymentsModelDao,
- new Function<PaymentModelDao, Payment>() {
- @Override
- public Payment apply(final PaymentModelDao paymentModelDao) {
- List<PaymentTransactionInfoPlugin> pluginInfo = null;
-
- if (withPluginInfo) {
- PaymentPluginApi pluginApi = paymentPluginByPaymentMethodId.get(paymentModelDao.getPaymentMethodId());
- if (pluginApi == null && !absentPlugins.contains(paymentModelDao.getPaymentMethodId())) {
- try {
- pluginApi = getPaymentProviderPlugin(paymentModelDao.getPaymentMethodId(), tenantContext);
- paymentPluginByPaymentMethodId.put(paymentModelDao.getPaymentMethodId(), pluginApi);
- } catch (final PaymentApiException e) {
- log.warn("Unable to retrieve pluginApi for payment method " + paymentModelDao.getPaymentMethodId());
- absentPlugins.add(paymentModelDao.getPaymentMethodId());
- }
- }
-
- pluginInfo = getPaymentTransactionInfoPluginsIfNeeded(pluginApi, paymentModelDao, context);
- }
-
- return toPayment(paymentModelDao, transactionsModelDao, pluginInfo, tenantContext);
- }
- });
+ final List<Payment> transformedPayments = Lists.<PaymentModelDao, Payment>transform(paymentsModelDao,
+ new Function<PaymentModelDao, Payment>() {
+ @Override
+ public Payment apply(final PaymentModelDao paymentModelDao) {
+ List<PaymentTransactionInfoPlugin> pluginInfo = null;
+
+ if (withPluginInfo) {
+ PaymentPluginApi pluginApi = paymentPluginByPaymentMethodId.get(paymentModelDao.getPaymentMethodId());
+ if (pluginApi == null && !absentPlugins.contains(paymentModelDao.getPaymentMethodId())) {
+ try {
+ pluginApi = getPaymentProviderPlugin(paymentModelDao.getPaymentMethodId(), tenantContext);
+ paymentPluginByPaymentMethodId.put(paymentModelDao.getPaymentMethodId(), pluginApi);
+ } catch (final PaymentApiException e) {
+ log.warn("Unable to retrieve pluginApi for payment method " + paymentModelDao.getPaymentMethodId());
+ absentPlugins.add(paymentModelDao.getPaymentMethodId());
+ }
+ }
+
+ pluginInfo = getPaymentTransactionInfoPluginsIfNeeded(pluginApi, paymentModelDao, context);
+ }
+
+ return toPayment(paymentModelDao, transactionsModelDao, pluginInfo, tenantContext);
+ }
+ });
+
+ // Copy the transformed list, so the transformation function is applied once (otherwise, the Janitor could be invoked multiple times)
+ return ImmutableList.<Payment>copyOf(transformedPayments);
}
public Payment getPayment(final UUID paymentId, final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
@@ -324,33 +330,144 @@ public class PaymentProcessor extends ProcessorBase {
);
}
- private Payment performOperation(final boolean isApiPayment, @Nullable final UUID attemptId,
- final TransactionType transactionType, final Account account,
- @Nullable final UUID paymentMethodId, @Nullable final UUID paymentId, @Nullable final UUID transactionId,
- @Nullable final BigDecimal amount, @Nullable final Currency currency,
- @Nullable final String paymentExternalKey, @Nullable final String paymentTransactionExternalKey,
- final boolean shouldLockAccountAndDispatch, @Nullable final OperationResult overridePluginOperationResult,
+ private Payment performOperation(final boolean isApiPayment,
+ @Nullable final UUID attemptId,
+ final TransactionType transactionType,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final UUID transactionId,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ @Nullable final String paymentExternalKey,
+ @Nullable final String paymentTransactionExternalKey,
+ final boolean shouldLockAccountAndDispatch,
+ @Nullable final OperationResult overridePluginOperationResult,
final Iterable<PluginProperty> properties,
- final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
- final UUID nonNullPaymentId = paymentAutomatonRunner.run(isApiPayment,
- transactionType,
- account,
- attemptId,
- paymentMethodId,
- paymentId,
- transactionId,
- paymentExternalKey,
- paymentTransactionExternalKey,
- amount,
- currency,
- shouldLockAccountAndDispatch,
- overridePluginOperationResult,
- properties,
- callContext,
- internalCallContext);
+ final CallContext callContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
+ final PaymentStateContext paymentStateContext = paymentAutomatonRunner.buildPaymentStateContext(isApiPayment,
+ transactionType,
+ account,
+ attemptId,
+ paymentMethodId != null ? paymentMethodId : account.getPaymentMethodId(),
+ paymentId,
+ transactionId,
+ paymentExternalKey,
+ paymentTransactionExternalKey,
+ amount,
+ currency,
+ shouldLockAccountAndDispatch,
+ overridePluginOperationResult,
+ properties,
+ callContext,
+ internalCallContext);
+ final PaymentAutomatonDAOHelper daoHelper = paymentAutomatonRunner.buildDaoHelper(paymentStateContext, internalCallContext);
+
+ String currentStateName = null;
+ if (paymentStateContext.getPaymentId() != null) {
+ PaymentModelDao paymentModelDao = daoHelper.getPayment();
+
+ // Sanity: verify the payment belongs to the right account (in case it was looked-up by payment or transaction external key)
+ if (!paymentModelDao.getAccountRecordId().equals(internalCallContext.getAccountRecordId())) {
+ // TODO 0.17.x New ErrorCode (it's not necessarily the transaction external key that matches)
+ throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
+ }
+
+ if (paymentStateContext.getTransactionId() != null || paymentStateContext.getPaymentTransactionExternalKey() != null) {
+ // If a transaction id or key is passed, we are maybe completing an existing transaction (unless a new key was provided)
+ final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment = daoHelper.getPaymentDao().getTransactionsForPayment(paymentStateContext.getPaymentId(), paymentStateContext.getInternalCallContext());
+ PaymentTransactionModelDao transactionToComplete = findTransactionToCompleteAndRunSanityChecks(paymentModelDao, paymentTransactionsForCurrentPayment, paymentStateContext, internalCallContext);
+
+ if (transactionToComplete != null) {
+ // For completion calls, always invoke the Janitor first to get the latest state. The state machine will then
+ // prevent disallowed transitions in case the state couldn't be fixed (or if it's already in a final state).
+ final PaymentPluginApi plugin = getPaymentProviderPlugin(paymentModelDao.getPaymentMethodId(), internalCallContext);
+ final List<PaymentTransactionInfoPlugin> pluginTransactions = getPaymentTransactionInfoPlugins(plugin, paymentModelDao, properties, callContext);
+ paymentModelDao = invokeJanitor(paymentModelDao, paymentTransactionsForCurrentPayment, pluginTransactions, internalCallContext);
+
+ final UUID transactionToCompleteId = transactionToComplete.getId();
+ transactionToComplete = Iterables.<PaymentTransactionModelDao>find(paymentTransactionsForCurrentPayment,
+ new Predicate<PaymentTransactionModelDao>() {
+ @Override
+ public boolean apply(final PaymentTransactionModelDao input) {
+ return transactionToCompleteId.equals(input.getId());
+ }
+ });
+
+ // We can't tell where we should be in the state machine - bail (cannot be enforced by the state machine unfortunately because UNKNOWN and PLUGIN_FAILURE are both treated as EXCEPTION)
+ if (transactionToComplete.getTransactionStatus() == TransactionStatus.UNKNOWN) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_OPERATION, paymentStateContext.getTransactionType(), transactionToComplete.getTransactionStatus());
+ }
+
+ paymentStateContext.setPaymentTransactionModelDao(transactionToComplete);
+ }
+ }
+
+ // Use the original payment method id of the payment being completed
+ paymentStateContext.setPaymentMethodId(paymentModelDao.getPaymentMethodId());
+ // We always take the last successful state name to permit retries on failures
+ currentStateName = paymentModelDao.getLastSuccessStateName();
+ }
+
+ // Sanity: no paymentMethodId was passed through API and account does not have a default paymentMethodId
+ if (paymentStateContext.getPaymentMethodId() == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD, paymentStateContext.getAccount().getId());
+ }
+
+ final UUID nonNullPaymentId = paymentAutomatonRunner.run(paymentStateContext, daoHelper, currentStateName, transactionType);
+
return getPayment(nonNullPaymentId, true, properties, callContext, internalCallContext);
}
+ private PaymentTransactionModelDao findTransactionToCompleteAndRunSanityChecks(final PaymentModelDao paymentModelDao,
+ final Iterable<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment,
+ final PaymentStateContext paymentStateContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
+ final Collection<PaymentTransactionModelDao> completionCandidates = new LinkedList<PaymentTransactionModelDao>();
+ for (final PaymentTransactionModelDao paymentTransactionModelDao : paymentTransactionsForCurrentPayment) {
+ // Check if we already have a transaction for that id or key
+ if (!(paymentStateContext.getTransactionId() != null && paymentTransactionModelDao.getId().equals(paymentStateContext.getTransactionId())) &&
+ !(paymentStateContext.getPaymentTransactionExternalKey() != null && paymentTransactionModelDao.getTransactionExternalKey().equals(paymentStateContext.getPaymentTransactionExternalKey()))) {
+ // Sanity: if not, prevent multiple PENDING transactions for initial calls (cannot be enforced by the state machine unfortunately)
+ if ((paymentTransactionModelDao.getTransactionType() == TransactionType.AUTHORIZE ||
+ paymentTransactionModelDao.getTransactionType() == TransactionType.PURCHASE ||
+ paymentTransactionModelDao.getTransactionType() == TransactionType.CREDIT) &&
+ paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PENDING) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_OPERATION, paymentTransactionModelDao.getTransactionType(), paymentModelDao.getStateName());
+ } else {
+ continue;
+ }
+ }
+
+ // Sanity: if we already have a transaction for that id or key, the transaction type must match
+ if (paymentTransactionModelDao.getTransactionType() != paymentStateContext.getTransactionType()) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, "transactionType", String.format("%s doesn't match existing transaction type %s", paymentStateContext.getTransactionType(), paymentTransactionModelDao.getTransactionType()));
+ }
+
+ // Sanity: verify we don't already have a successful transaction for that key (chargeback reversals are a bit special, it's the only transaction type we can revert)
+ if (paymentTransactionModelDao.getTransactionExternalKey().equals(paymentStateContext.getPaymentTransactionExternalKey()) &&
+ paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.SUCCESS &&
+ paymentTransactionModelDao.getTransactionType() != TransactionType.CHARGEBACK) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
+ }
+
+ // Sanity: don't share keys across accounts
+ if (paymentTransactionModelDao.getTransactionExternalKey().equals(paymentStateContext.getPaymentTransactionExternalKey()) &&
+ !paymentTransactionModelDao.getAccountRecordId().equals(internalCallContext.getAccountRecordId())) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
+ }
+
+ // UNKNOWN transactions are potential candidates, we'll invoke the Janitor first though
+ if (paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PENDING || paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.UNKNOWN) {
+ completionCandidates.add(paymentTransactionModelDao);
+ }
+ }
+
+ Preconditions.checkState(Iterables.<PaymentTransactionModelDao>size(completionCandidates) <= 1, "There should be at most one completion candidate");
+ return Iterables.<PaymentTransactionModelDao>getLast(completionCandidates, null);
+ }
+
// Used in bulk get API (getAccountPayments / getPayments)
private List<PaymentTransactionInfoPlugin> getPaymentTransactionInfoPluginsIfNeeded(@Nullable final PaymentPluginApi pluginApi, final PaymentModelDao paymentModelDao, final TenantContext context) {
if (pluginApi == null) {
@@ -399,15 +516,7 @@ public class PaymentProcessor extends ProcessorBase {
return toPayment(paymentModelDao, transactionsForPayment, pluginTransactions, tenantContextWithAccountRecordId);
}
- // Used in bulk get API (getAccountPayments)
- private Payment toPayment(final PaymentModelDao curPaymentModelDao, final Iterable<PaymentTransactionModelDao> curTransactionsModelDao, @Nullable final Iterable<PaymentTransactionInfoPlugin> pluginTransactions, final InternalTenantContext internalTenantContext) {
- final Ordering<PaymentTransaction> perPaymentTransactionOrdering = Ordering.<PaymentTransaction>from(new Comparator<PaymentTransaction>() {
- @Override
- public int compare(final PaymentTransaction o1, final PaymentTransaction o2) {
- return o1.getEffectiveDate().compareTo(o2.getEffectiveDate());
- }
- });
-
+ private PaymentModelDao invokeJanitor(final PaymentModelDao curPaymentModelDao, final Collection<PaymentTransactionModelDao> curTransactionsModelDao, @Nullable final Iterable<PaymentTransactionInfoPlugin> pluginTransactions, final InternalTenantContext internalTenantContext) {
// Need to filter for optimized codepaths looking up by account_record_id
final Iterable<PaymentTransactionModelDao> filteredTransactions = Iterables.filter(curTransactionsModelDao, new Predicate<PaymentTransactionModelDao>() {
@Override
@@ -417,7 +526,7 @@ public class PaymentProcessor extends ProcessorBase {
});
PaymentModelDao newPaymentModelDao = curPaymentModelDao;
- final Collection<PaymentTransaction> transactions = new LinkedList<PaymentTransaction>();
+ final Collection<PaymentTransactionModelDao> transactionsModelDao = new LinkedList<PaymentTransactionModelDao>();
for (final PaymentTransactionModelDao curPaymentTransactionModelDao : filteredTransactions) {
PaymentTransactionModelDao newPaymentTransactionModelDao = curPaymentTransactionModelDao;
@@ -432,6 +541,23 @@ public class PaymentProcessor extends ProcessorBase {
}
}
+ transactionsModelDao.add(newPaymentTransactionModelDao);
+ }
+
+ curTransactionsModelDao.clear();
+ curTransactionsModelDao.addAll(transactionsModelDao);
+
+ return newPaymentModelDao;
+ }
+
+ // Used in bulk get API (getAccountPayments)
+ private Payment toPayment(final PaymentModelDao curPaymentModelDao, final Collection<PaymentTransactionModelDao> curTransactionsModelDao, @Nullable final Iterable<PaymentTransactionInfoPlugin> pluginTransactions, final InternalTenantContext internalTenantContext) {
+ final Collection<PaymentTransactionModelDao> transactionsModelDao = new LinkedList<PaymentTransactionModelDao>(curTransactionsModelDao);
+ final PaymentModelDao newPaymentModelDao = invokeJanitor(curPaymentModelDao, transactionsModelDao, pluginTransactions, internalTenantContext);
+
+ final Collection<PaymentTransaction> transactions = new LinkedList<PaymentTransaction>();
+ for (final PaymentTransactionModelDao newPaymentTransactionModelDao : transactionsModelDao) {
+ final PaymentTransactionInfoPlugin paymentTransactionInfoPlugin = findPaymentTransactionInfoPlugin(newPaymentTransactionModelDao, pluginTransactions);
final PaymentTransaction transaction = new DefaultPaymentTransaction(newPaymentTransactionModelDao.getId(),
newPaymentTransactionModelDao.getAttemptId(),
newPaymentTransactionModelDao.getTransactionExternalKey(),
@@ -451,14 +577,21 @@ public class PaymentProcessor extends ProcessorBase {
transactions.add(transaction);
}
+ final Ordering<PaymentTransaction> perPaymentTransactionOrdering = Ordering.<PaymentTransaction>from(new Comparator<PaymentTransaction>() {
+ @Override
+ public int compare(final PaymentTransaction o1, final PaymentTransaction o2) {
+ return o1.getEffectiveDate().compareTo(o2.getEffectiveDate());
+ }
+ });
final List<PaymentTransaction> sortedTransactions = perPaymentTransactionOrdering.immutableSortedCopy(transactions);
- return new DefaultPayment(curPaymentModelDao.getId(),
- curPaymentModelDao.getCreatedDate(),
- curPaymentModelDao.getUpdatedDate(),
- curPaymentModelDao.getAccountId(),
- curPaymentModelDao.getPaymentMethodId(),
- curPaymentModelDao.getPaymentNumber(),
- curPaymentModelDao.getExternalKey(),
+
+ return new DefaultPayment(newPaymentModelDao.getId(),
+ newPaymentModelDao.getCreatedDate(),
+ newPaymentModelDao.getUpdatedDate(),
+ newPaymentModelDao.getAccountId(),
+ newPaymentModelDao.getPaymentMethodId(),
+ newPaymentModelDao.getPaymentNumber(),
+ newPaymentModelDao.getExternalKey(),
sortedTransactions);
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java
index 9a87984..dcd1707 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java
@@ -17,11 +17,14 @@
package org.killbill.billing.payment.core.sm.control;
+import java.util.List;
+
import org.joda.time.DateTime;
import org.killbill.automaton.OperationException;
import org.killbill.automaton.State;
import org.killbill.automaton.State.LeavingStateCallback;
import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.api.TransactionType;
import org.killbill.billing.payment.core.sm.PaymentStateContext;
import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner;
@@ -38,6 +41,10 @@ import com.google.common.collect.ImmutableList;
public class DefaultControlInitiated implements LeavingStateCallback {
+ private static final ImmutableList<TransactionStatus> TRANSIENT_TRANSACTION_STATUSES = ImmutableList.<TransactionStatus>builder().add(TransactionStatus.PENDING)
+ .add(TransactionStatus.UNKNOWN)
+ .build();
+
private final PluginControlPaymentAutomatonRunner pluginControlPaymentAutomatonRunner;
private final PaymentStateControlContext stateContext;
private final State initialState;
@@ -59,6 +66,19 @@ public class DefaultControlInitiated implements LeavingStateCallback {
public void leavingState(final State state) throws OperationException {
final DateTime utcNow = pluginControlPaymentAutomatonRunner.getClock().getUTCNow();
+ // Retrieve the associated payment transaction, if any
+ PaymentTransactionModelDao paymentTransactionModelDaoCandidate = null;
+ if (stateContext.getTransactionId() != null) {
+ paymentTransactionModelDaoCandidate = paymentDao.getPaymentTransaction(stateContext.getTransactionId(), stateContext.getInternalCallContext());
+ Preconditions.checkNotNull(paymentTransactionModelDaoCandidate, "paymentTransaction cannot be null for id " + stateContext.getTransactionId());
+ } else if (stateContext.getPaymentTransactionExternalKey() != null) {
+ final List<PaymentTransactionModelDao> paymentTransactionModelDaos = paymentDao.getPaymentTransactionsByExternalKey(stateContext.getPaymentTransactionExternalKey(), stateContext.getInternalCallContext());
+ if (!paymentTransactionModelDaos.isEmpty()) {
+ paymentTransactionModelDaoCandidate = paymentTransactionModelDaos.get(paymentTransactionModelDaos.size() - 1);
+ }
+ }
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentTransactionModelDaoCandidate != null && TRANSIENT_TRANSACTION_STATUSES.contains(paymentTransactionModelDaoCandidate.getTransactionStatus()) ? paymentTransactionModelDaoCandidate : null;
+
if (stateContext.getPaymentId() != null && stateContext.getPaymentExternalKey() == null) {
final PaymentModelDao payment = paymentDao.getPayment(stateContext.getPaymentId(), stateContext.getInternalCallContext());
Preconditions.checkNotNull(payment, "payment cannot be null for id " + stateContext.getPaymentId());
@@ -67,9 +87,8 @@ public class DefaultControlInitiated implements LeavingStateCallback {
} else if (stateContext.getPaymentExternalKey() == null) {
stateContext.setPaymentExternalKey(UUIDs.randomUUID().toString());
}
- if (stateContext.getTransactionId() != null && stateContext.getPaymentTransactionExternalKey() == null) {
- final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(stateContext.getTransactionId(), stateContext.getInternalCallContext());
- Preconditions.checkNotNull(paymentTransactionModelDao, "paymentTransaction cannot be null for id " + stateContext.getTransactionId());
+
+ if (paymentTransactionModelDao != null) {
stateContext.setPaymentTransactionExternalKey(paymentTransactionModelDao.getTransactionExternalKey());
} else if (stateContext.getPaymentTransactionExternalKey() == null) {
stateContext.setPaymentTransactionExternalKey(UUIDs.randomUUID().toString());
@@ -82,20 +101,26 @@ public class DefaultControlInitiated implements LeavingStateCallback {
if (state.getName().equals(initialState.getName()) || state.getName().equals(retriedState.getName())) {
try {
- //
- // We don't serialize any properties at this stage to avoid serializing sensitive information.
- // However, if after going through the control plugins, the attempt end up in RETRIED state,
- // the properties will be serialized in the enteringState callback (any plugin that sets a
- // retried date is responsible to correctly remove sensitive information such as CVV, ...)
- //
- final byte[] serializedProperties = PluginPropertySerializer.serialize(ImmutableList.<PluginProperty>of());
- final PaymentAttemptModelDao attempt = new PaymentAttemptModelDao(stateContext.getAccount().getId(), stateContext.getPaymentMethodId(),
- utcNow, utcNow, stateContext.getPaymentExternalKey(), stateContext.getTransactionId(),
- stateContext.getPaymentTransactionExternalKey(), transactionType, initialState.getName(),
- stateContext.getAmount(), stateContext.getCurrency(),
- stateContext.getPaymentControlPluginNames(), serializedProperties);
-
- pluginControlPaymentAutomatonRunner.getPaymentDao().insertPaymentAttemptWithProperties(attempt, stateContext.getInternalCallContext());
+ final PaymentAttemptModelDao attempt;
+ if (paymentTransactionModelDao != null && paymentTransactionModelDao.getAttemptId() != null) {
+ attempt = pluginControlPaymentAutomatonRunner.getPaymentDao().getPaymentAttempt(paymentTransactionModelDao.getAttemptId(), stateContext.getInternalCallContext());
+ Preconditions.checkNotNull(attempt, "attempt cannot be null for id " + paymentTransactionModelDao.getAttemptId());
+ } else {
+ //
+ // We don't serialize any properties at this stage to avoid serializing sensitive information.
+ // However, if after going through the control plugins, the attempt end up in RETRIED state,
+ // the properties will be serialized in the enteringState callback (any plugin that sets a
+ // retried date is responsible to correctly remove sensitive information such as CVV, ...)
+ //
+ final byte[] serializedProperties = PluginPropertySerializer.serialize(ImmutableList.<PluginProperty>of());
+
+ attempt = new PaymentAttemptModelDao(stateContext.getAccount().getId(), stateContext.getPaymentMethodId(),
+ utcNow, utcNow, stateContext.getPaymentExternalKey(), stateContext.getTransactionId(),
+ stateContext.getPaymentTransactionExternalKey(), transactionType, initialState.getName(),
+ stateContext.getAmount(), stateContext.getCurrency(),
+ stateContext.getPaymentControlPluginNames(), serializedProperties);
+ pluginControlPaymentAutomatonRunner.getPaymentDao().insertPaymentAttemptWithProperties(attempt, stateContext.getInternalCallContext());
+ }
stateContext.setAttemptId(attempt.getId());
} catch (final PluginPropertySerializerException e) {
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java
index c6545dc..fa55ff0 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java
@@ -57,6 +57,10 @@ public class PaymentAutomatonDAOHelper {
private final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry;
private final PersistentBus eventBus;
+ // Cached
+ private String pluginName = null;
+ private PaymentPluginApi paymentPluginApi = null;
+
// Used to build new payments and transactions
public PaymentAutomatonDAOHelper(final PaymentStateContext paymentStateContext,
final DateTime utcNow, final PaymentDao paymentDao,
@@ -128,6 +132,7 @@ public class PaymentAutomatonDAOHelper {
final String lastSuccessPaymentState = paymentSMHelper.isSuccessState(currentPaymentStateName) ? currentPaymentStateName : null;
paymentDao.updatePaymentAndTransactionOnCompletion(paymentStateContext.getAccount().getId(),
+ paymentStateContext.getAttemptId(),
paymentStateContext.getPaymentId(),
paymentStateContext.getTransactionType(),
currentPaymentStateName,
@@ -145,27 +150,24 @@ public class PaymentAutomatonDAOHelper {
}
public String getPaymentProviderPluginName() throws PaymentApiException {
+ if (pluginName != null) {
+ return pluginName;
+ }
+
final UUID paymentMethodId = paymentStateContext.getPaymentMethodId();
final PaymentMethodModelDao methodDao = paymentDao.getPaymentMethodIncludedDeleted(paymentMethodId, internalCallContext);
if (methodDao == null) {
throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD, paymentMethodId);
}
- return methodDao.getPluginName();
+ pluginName = methodDao.getPluginName();
+ return pluginName;
}
- public PaymentPluginApi getPaymentProviderPlugin() throws PaymentApiException {
+ public PaymentPluginApi getPaymentPluginApi() throws PaymentApiException {
final String pluginName = getPaymentProviderPluginName();
return getPaymentPluginApi(pluginName);
}
- public PaymentPluginApi getPaymentPluginApi(final String pluginName) throws PaymentApiException {
- final PaymentPluginApi pluginApi = pluginRegistry.getServiceForName(pluginName);
- if (pluginApi == null) {
- throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_PLUGIN, pluginName);
- }
- return pluginApi;
- }
-
public PaymentModelDao getPayment() throws PaymentApiException {
final PaymentModelDao paymentModelDao;
paymentModelDao = paymentDao.getPayment(paymentStateContext.getPaymentId(), internalCallContext);
@@ -183,6 +185,18 @@ public class PaymentAutomatonDAOHelper {
return paymentDao;
}
+ private PaymentPluginApi getPaymentPluginApi(final String pluginName) throws PaymentApiException {
+ if (paymentPluginApi != null) {
+ return paymentPluginApi;
+ }
+
+ paymentPluginApi = pluginRegistry.getServiceForName(pluginName);
+ if (paymentPluginApi == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_PLUGIN, pluginName);
+ }
+ return paymentPluginApi;
+ }
+
private PaymentModelDao buildNewPaymentModelDao() {
final DateTime createdDate = utcNow;
final DateTime updatedDate = utcNow;
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java
index 99c913b..012d7c0 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * 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
@@ -18,9 +18,9 @@
package org.killbill.billing.payment.core.sm;
import java.math.BigDecimal;
+import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
import javax.inject.Inject;
@@ -35,6 +35,7 @@ import org.killbill.automaton.State;
import org.killbill.automaton.State.EnteringStateCallback;
import org.killbill.automaton.State.LeavingStateCallback;
import org.killbill.automaton.StateMachine;
+import org.killbill.automaton.StateMachineConfig;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.callcontext.InternalCallContext;
@@ -42,6 +43,7 @@ import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.osgi.api.OSGIServiceRegistration;
import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.api.TransactionType;
import org.killbill.billing.payment.core.PaymentExecutors;
import org.killbill.billing.payment.core.sm.payments.AuthorizeCompleted;
@@ -67,8 +69,8 @@ import org.killbill.billing.payment.core.sm.payments.VoidInitiated;
import org.killbill.billing.payment.core.sm.payments.VoidOperation;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.dao.PaymentModelDao;
+import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import org.killbill.billing.payment.dispatcher.PluginDispatcher;
-import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.config.definition.PaymentConfig;
@@ -76,10 +78,7 @@ import org.killbill.bus.api.PersistentBus;
import org.killbill.clock.Clock;
import org.killbill.commons.locker.GlobalLocker;
-import com.google.common.base.Objects;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Iterables;
+import com.google.common.base.MoreObjects;
public class PaymentAutomatonRunner {
@@ -89,6 +88,7 @@ public class PaymentAutomatonRunner {
protected final PluginDispatcher<OperationResult> paymentPluginDispatcher;
protected final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry;
protected final Clock clock;
+
private final PersistentBus eventBus;
private final PaymentConfig paymentConfig;
@@ -110,42 +110,57 @@ public class PaymentAutomatonRunner {
this.paymentConfig = paymentConfig;
final long paymentPluginTimeoutSec = TimeUnit.SECONDS.convert(paymentConfig.getPaymentPluginTimeout().getPeriod(), paymentConfig.getPaymentPluginTimeout().getUnit());
this.paymentPluginDispatcher = new PluginDispatcher<OperationResult>(paymentPluginTimeoutSec, executors);
+ }
+
+ public PaymentStateContext buildPaymentStateContext(final boolean isApiPayment,
+ final TransactionType transactionType,
+ final Account account,
+ @Nullable final UUID attemptId,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final UUID transactionId,
+ @Nullable final String paymentExternalKey,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final boolean shouldLockAccount,
+ final OperationResult overridePluginOperationResult,
+ final Iterable<PluginProperty> properties,
+ final CallContext callContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
+ // Retrieve the payment id from the payment external key if needed
+ final UUID effectivePaymentId = paymentId != null ? paymentId : retrievePaymentId(paymentExternalKey, paymentTransactionExternalKey, internalCallContext);
+ return new PaymentStateContext(isApiPayment,
+ effectivePaymentId,
+ transactionId,
+ attemptId,
+ paymentExternalKey,
+ paymentTransactionExternalKey,
+ transactionType,
+ account,
+ paymentMethodId,
+ amount,
+ currency,
+ shouldLockAccount,
+ overridePluginOperationResult,
+ properties,
+ internalCallContext,
+ callContext);
}
- public UUID run(final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID attemptId, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final UUID transactionId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
- @Nullable final BigDecimal amount, @Nullable final Currency currency,
- final boolean shouldLockAccount, final OperationResult overridePluginOperationResult, final Iterable<PluginProperty> properties,
- final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
+ public PaymentAutomatonDAOHelper buildDaoHelper(final PaymentStateContext paymentStateContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
final DateTime utcNow = clock.getUTCNow();
- // Retrieve the payment id from the payment external key if needed
- final UUID effectivePaymentId = paymentId != null ? paymentId : retrievePaymentId(paymentExternalKey, internalCallContext);
-
- final PaymentStateContext paymentStateContext = new PaymentStateContext(isApiPayment, effectivePaymentId, transactionId, attemptId, paymentExternalKey, paymentTransactionExternalKey, transactionType,
- account, paymentMethodId, amount, currency, shouldLockAccount, overridePluginOperationResult, properties, internalCallContext, callContext);
-
- final PaymentAutomatonDAOHelper daoHelper = new PaymentAutomatonDAOHelper(paymentStateContext, utcNow, paymentDao, pluginRegistry, internalCallContext, eventBus, paymentSMHelper);
-
- final UUID effectivePaymentMethodId;
- final String currentStateName;
- if (effectivePaymentId != null) {
- final PaymentModelDao paymentModelDao = daoHelper.getPayment();
- effectivePaymentMethodId = paymentModelDao.getPaymentMethodId();
- currentStateName = paymentModelDao.getLastSuccessStateName() != null ? paymentModelDao.getLastSuccessStateName() : paymentSMHelper.getInitStateNameForTransaction();
-
- // Check for illegal states (should never happen)
- Preconditions.checkState(currentStateName != null, "State name cannot be null for payment " + effectivePaymentId);
- Preconditions.checkState(paymentMethodId == null || effectivePaymentMethodId.equals(paymentMethodId), "Specified payment method id " + paymentMethodId + " doesn't match the one on the payment " + effectivePaymentMethodId);
- } else {
- // If the payment method is not specified, retrieve the default one on the account; it could still be null, in which case
- //
- effectivePaymentMethodId = paymentMethodId != null ? paymentMethodId : account.getPaymentMethodId();
- currentStateName = paymentSMHelper.getInitStateNameForTransaction();
- }
+ return new PaymentAutomatonDAOHelper(paymentStateContext, utcNow, paymentDao, pluginRegistry, internalCallContext, eventBus, paymentSMHelper);
+ }
- paymentStateContext.setPaymentMethodId(effectivePaymentMethodId);
+ public UUID run(final PaymentStateContext paymentStateContext,
+ final PaymentAutomatonDAOHelper daoHelper,
+ @Nullable final String currentStateNameOrNull,
+ final TransactionType transactionType) throws PaymentApiException {
+ final String currentStateName = MoreObjects.firstNonNull(currentStateNameOrNull, paymentSMHelper.getInitStateNameForTransaction());
final OperationCallback operationCallback;
final LeavingStateCallback leavingStateCallback;
@@ -190,7 +205,7 @@ public class PaymentAutomatonRunner {
throw new IllegalStateException("Unsupported transaction type " + transactionType);
}
- runStateMachineOperation(currentStateName, transactionType, leavingStateCallback, operationCallback, enteringStateCallback, account.getId(), getInvoiceId(properties));
+ runStateMachineOperation(currentStateName, transactionType, leavingStateCallback, operationCallback, enteringStateCallback, paymentStateContext, daoHelper);
return paymentStateContext.getPaymentId();
}
@@ -206,45 +221,67 @@ public class PaymentAutomatonRunner {
return clock;
}
- protected void runStateMachineOperation(final String initialStateName, final TransactionType transactionType,
- final LeavingStateCallback leavingStateCallback, final OperationCallback operationCallback, final EnteringStateCallback enteringStateCallback,
- final UUID accountId, final String invoiceId) throws PaymentApiException {
+ private void runStateMachineOperation(final String initialStateName,
+ final TransactionType transactionType,
+ final LeavingStateCallback leavingStateCallback,
+ final OperationCallback operationCallback,
+ final EnteringStateCallback enteringStateCallback,
+ final PaymentStateContext paymentStateContext,
+ final PaymentAutomatonDAOHelper daoHelper) throws PaymentApiException {
try {
- final StateMachine initialStateMachine = paymentSMHelper.getStateMachineForStateName(initialStateName);
+ final StateMachineConfig stateMachineConfig = paymentSMHelper.getStateMachineConfig(daoHelper.getPaymentProviderPluginName(), paymentStateContext.getInternalCallContext());
+ final StateMachine initialStateMachine = stateMachineConfig.getStateMachineForState(initialStateName);
final State initialState = initialStateMachine.getState(initialStateName);
- final Operation operation = paymentSMHelper.getOperationForTransaction(transactionType);
+ final Operation operation = paymentSMHelper.getOperationForTransaction(stateMachineConfig, transactionType);
initialState.runOperation(operation, operationCallback, enteringStateCallback, leavingStateCallback);
} catch (final MissingEntryException e) {
throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INVALID_OPERATION, transactionType, initialStateName);
} catch (final OperationException e) {
if (e.getCause() == null) {
- throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), ""));
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, MoreObjects.firstNonNull(e.getMessage(), ""));
} else if (e.getCause() instanceof PaymentApiException) {
throw (PaymentApiException) e.getCause();
} else {
- throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), ""));
+ throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, MoreObjects.firstNonNull(e.getMessage(), ""));
}
}
}
- private String getInvoiceId(final Iterable<PluginProperty> properties) {
- final PluginProperty invoiceProperty = Iterables.tryFind(properties, new Predicate<PluginProperty>() {
- @Override
- public boolean apply(final PluginProperty input) {
- return InvoicePaymentControlPluginApi.PROP_IPCD_INVOICE_ID.equals(input.getKey());
+ // TODO Could we cache these to avoid extra queries in PaymentAutomatonDAOHelper?
+ private UUID retrievePaymentId(@Nullable final String paymentExternalKey, @Nullable final String paymentTransactionExternalKey, final InternalCallContext internalCallContext) throws PaymentApiException {
+ if (paymentExternalKey != null) {
+ final PaymentModelDao payment = paymentDao.getPaymentByExternalKey(paymentExternalKey, internalCallContext);
+ if (payment != null) {
+ return payment.getId();
}
- }).orNull();
-
- return invoiceProperty == null || invoiceProperty.getValue() == null ? null : invoiceProperty.getValue().toString();
- }
+ }
- private UUID retrievePaymentId(@Nullable final String paymentExternalKey, final InternalCallContext internalCallContext) {
- if (paymentExternalKey == null) {
+ if (paymentTransactionExternalKey == null) {
return null;
}
- final PaymentModelDao payment = paymentDao.getPaymentByExternalKey(paymentExternalKey, internalCallContext);
- return payment == null ? null : payment.getId();
+ final List<PaymentTransactionModelDao> paymentTransactionModelDaos = paymentDao.getPaymentTransactionsByExternalKey(paymentTransactionExternalKey, internalCallContext);
+ for (final PaymentTransactionModelDao paymentTransactionModelDao : paymentTransactionModelDaos) {
+ if (paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.SUCCESS ||
+ paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PENDING ||
+ paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.UNKNOWN) {
+ return paymentTransactionModelDao.getPaymentId();
+ }
+ }
+
+ UUID paymentIdCandidate = null;
+ for (final PaymentTransactionModelDao paymentTransactionModelDao : paymentTransactionModelDaos) {
+ if (paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PAYMENT_FAILURE ||
+ paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PLUGIN_FAILURE) {
+ if (paymentIdCandidate == null) {
+ paymentIdCandidate = paymentTransactionModelDao.getPaymentId();
+ } else if (!paymentIdCandidate.equals(paymentTransactionModelDao.getPaymentId())) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, "Multiple failed payments sharing the same transaction external key - this should never happen");
+ }
+ }
+ }
+
+ return paymentIdCandidate;
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java
index 0b08255..5e07f04 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java
@@ -17,7 +17,14 @@
package org.killbill.billing.payment.core.sm.payments;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.killbill.automaton.OperationException;
import org.killbill.automaton.OperationResult;
+import org.killbill.automaton.State;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.core.sm.PaymentAutomatonDAOHelper;
@@ -25,6 +32,7 @@ import org.killbill.billing.payment.core.sm.PaymentStateContext;
import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
public class ChargebackInitiated extends PaymentLeavingStateCallback {
@@ -34,33 +42,35 @@ public class ChargebackInitiated extends PaymentLeavingStateCallback {
}
@Override
- protected void validatePaymentIdAndTransactionType(final Iterable<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
- if (OperationResult.FAILURE.equals(paymentStateContext.getOverridePluginOperationResult()) && !existingPaymentTransactions.iterator().hasNext()) {
- // Chargeback reversals can only happen after a successful chargeback
- throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_SUCCESS_PAYMENT, paymentStateContext.getPaymentId());
- }
- super.validatePaymentIdAndTransactionType(existingPaymentTransactions);
- }
+ public void leavingState(final State oldState) throws OperationException {
+ // Sanity: chargeback reversals can only happen after a successful chargeback
+ if (OperationResult.FAILURE.equals(paymentStateContext.getOverridePluginOperationResult())) {
+ final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment = paymentStateContext.getPaymentId() != null ?
+ daoHelper.getPaymentDao().getTransactionsForPayment(paymentStateContext.getPaymentId(), paymentStateContext.getInternalCallContext()) :
+ ImmutableList.<PaymentTransactionModelDao>of();
+ final Iterable<PaymentTransactionModelDao> existingPaymentTransactionsForTransactionIdOrKey = filterExistingPaymentTransactionsForTransactionIdOrKey(paymentTransactionsForCurrentPayment, paymentStateContext.getTransactionId(), paymentStateContext.getPaymentTransactionExternalKey());
- @Override
- protected void validateUniqueTransactionExternalKey(final Iterable<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
- // If no key specified, system will allocate a unique one later, there is nothing to check
- if (paymentStateContext.getPaymentTransactionExternalKey() == null) {
- return;
+ if (Iterables.<PaymentTransactionModelDao>isEmpty(existingPaymentTransactionsForTransactionIdOrKey)) {
+ // Chargeback reversals can only happen after a successful chargeback
+ throw new OperationException(new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_SUCCESS_PAYMENT, paymentStateContext.getPaymentId()));
+ }
}
- // The main difference with the default implementation is that an existing transaction in a SUCCESS state can exist (chargeback reversal)
- if (Iterables.any(existingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
+ super.leavingState(oldState);
+ }
+
+ private Iterable<PaymentTransactionModelDao> filterExistingPaymentTransactionsForTransactionIdOrKey(final Iterable<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment, @Nullable final UUID paymentTransactionId, @Nullable final String paymentTransactionExternalKey) {
+ return Iterables.filter(paymentTransactionsForCurrentPayment, new Predicate<PaymentTransactionModelDao>() {
@Override
public boolean apply(final PaymentTransactionModelDao input) {
- // An existing transaction for a different payment (to do really well, we should also check on paymentExternalKey which is not available here)
- return (paymentStateContext.getPaymentId() != null && input.getPaymentId().compareTo(paymentStateContext.getPaymentId()) != 0) ||
- // Or, an existing transaction for a different account.
- (!input.getAccountRecordId().equals(paymentStateContext.getInternalCallContext().getAccountRecordId()));
-
+ if (paymentTransactionId != null && input.getId().equals(paymentTransactionId)) {
+ return true;
+ }
+ if (paymentTransactionExternalKey != null && input.getTransactionExternalKey().equals(paymentTransactionExternalKey)) {
+ return true;
+ }
+ return false;
}
- })) {
- throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
- }
+ });
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java
index 831b349..90f8599 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
*
* Groupon 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
@@ -17,31 +17,19 @@
package org.killbill.billing.payment.core.sm.payments;
-import java.util.List;
-import java.util.UUID;
-
-import javax.annotation.Nullable;
-
import org.killbill.automaton.OperationException;
import org.killbill.automaton.State;
import org.killbill.automaton.State.LeavingStateCallback;
-import org.killbill.billing.ErrorCode;
import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.TransactionStatus;
-import org.killbill.billing.payment.api.TransactionType;
import org.killbill.billing.payment.core.sm.PaymentAutomatonDAOHelper;
import org.killbill.billing.payment.core.sm.PaymentStateContext;
-import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.google.common.base.Predicate;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-
public abstract class PaymentLeavingStateCallback implements LeavingStateCallback {
- private final Logger logger = LoggerFactory.getLogger(PaymentLeavingStateCallback.class);
+ private static final Logger logger = LoggerFactory.getLogger(PaymentLeavingStateCallback.class);
protected final PaymentAutomatonDAOHelper daoHelper;
protected final PaymentStateContext paymentStateContext;
@@ -55,143 +43,13 @@ public abstract class PaymentLeavingStateCallback implements LeavingStateCallbac
public void leavingState(final State oldState) throws OperationException {
logger.debug("Leaving state {}", oldState.getName());
- // Create or update the payment and transaction
try {
- // No paymentMethodId was passed through API and account does not have a default paymentMethodId
- if (paymentStateContext.getPaymentMethodId() == null) {
- throw new PaymentApiException(ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD, paymentStateContext.getAccount().getId());
+ // We always create a new transaction (even in case of PAYMENT_FAILURE or PLUGIN_FAILURE) except for PENDING payments (completion)
+ if (paymentStateContext.getPaymentTransactionModelDao() == null || paymentStateContext.getPaymentTransactionModelDao().getTransactionStatus() != TransactionStatus.PENDING) {
+ daoHelper.createNewPaymentTransaction();
}
-
- // If we were given a paymentId (or existing paymentExternalId -> effectivePaymentId) we first fetch existing transactions (required for sanity and handling PENDING transactions)
- final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment = paymentStateContext.getPaymentId() != null ?
- daoHelper.getPaymentDao().getTransactionsForPayment(paymentStateContext.getPaymentId(), paymentStateContext.getInternalCallContext()) :
- ImmutableList.<PaymentTransactionModelDao>of();
-
- //
- // Extract existing transaction matching the transactionId if specified (for e.g notifyPendingTransactionOfStateChanged), or based on transactionExternalKey
- //
- final Iterable<PaymentTransactionModelDao> existingPaymentTransactionsForTransactionIdOrKey = filterExistingPaymentTransactionsForTransactionIdOrKey(paymentTransactionsForCurrentPayment, paymentStateContext.getTransactionId(), paymentStateContext.getPaymentTransactionExternalKey());
-
- // Validate the payment transactions belong to the right payment
- validatePaymentIdAndTransactionType(existingPaymentTransactionsForTransactionIdOrKey);
-
- // Validate some constraints on the unicity of that paymentTransactionExternalKey
- validateUniqueTransactionExternalKey(existingPaymentTransactionsForTransactionIdOrKey);
-
- //
- // Handle PENDING case:
- // a) If we have a PENDING transaction for the same (payment transaction) key, this is a completion and we want to re-use the same transaction
- // b) If we have a PENDING transaction for a different (payment transaction) key, and for an initial request (AUTH, PURCHASE, CREDIT), we FAIL the request
- // (unfortunately this cannot be caught by the state machine because the transition XXX_PENDING -> _SUCCESS needs to be allowed and this is irrespective of the keys)
- // c) If we have a PENDING transaction for a different (payment transaction) key, and for other follow-up request (CAPTURE, REFUND, ..), we ignore it and create a new transaction
- //
- final Iterable<PaymentTransactionModelDao> pendingTransactionsForPaymentAndTransactionType = filterPendingTransactionsForPaymentAndTransactionType(paymentTransactionsForCurrentPayment, paymentStateContext.getTransactionType());
-
- // Case b)
- validateUniqueInitialPendingTransaction(pendingTransactionsForPaymentAndTransactionType, paymentStateContext.getTransactionType(), paymentStateContext.getPaymentTransactionExternalKey());
-
-
- final PaymentTransactionModelDao pendingPaymentTransaction = filterPendingTransactionsForTransactionKey(pendingTransactionsForPaymentAndTransactionType, paymentStateContext.getPaymentTransactionExternalKey());
- if (pendingPaymentTransaction != null) {
- // Case a) Set the current paymentTransaction in the context (needed for the state machine logic)
- paymentStateContext.setPaymentTransactionModelDao(pendingPaymentTransaction);
- return;
- }
-
- // At this point we are left with PAYMENT_FAILURE, PLUGIN_FAILURE or nothing, and we validated the uniqueness of the paymentTransactionExternalKey so we will create a new row
- daoHelper.createNewPaymentTransaction();
-
- } catch (PaymentApiException e) {
+ } catch (final PaymentApiException e) {
throw new OperationException(e);
}
}
-
- private void validateUniqueInitialPendingTransaction(final Iterable<PaymentTransactionModelDao> pendingTransactionsForPaymentAndTransactionType, final TransactionType transactionType, final String paymentTransactionExternalKey) {
- if (transactionType != TransactionType.AUTHORIZE &&
- transactionType != TransactionType.PURCHASE &&
- transactionType != TransactionType.CREDIT) {
- return;
- }
-
- final PaymentTransactionModelDao existingPendingTransactionForDifferentKey = Iterables.tryFind(pendingTransactionsForPaymentAndTransactionType, new Predicate<PaymentTransactionModelDao>() {
- @Override
- public boolean apply(final PaymentTransactionModelDao input) {
- return !input.getTransactionExternalKey().equals(paymentTransactionExternalKey);
- }
- }).orNull();
- if (existingPendingTransactionForDifferentKey != null) {
- // We are missing ErrorCode PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS (should be fixed in 0.17.0. See #525)
- throw new RuntimeException(String.format("Failed to create another initial transaction for paymentId='%s' : Existing PENDING transactionId='%s'",
- existingPendingTransactionForDifferentKey.getPaymentId(), existingPendingTransactionForDifferentKey.getId()));
- }
- }
-
- protected Iterable<PaymentTransactionModelDao> filterExistingPaymentTransactionsForTransactionIdOrKey(final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment, @Nullable final UUID paymentTransactionId, @Nullable final String paymentTransactionExternalKey) throws PaymentApiException {
- return Iterables.filter(paymentTransactionsForCurrentPayment, new Predicate<PaymentTransactionModelDao>() {
- @Override
- public boolean apply(final PaymentTransactionModelDao input) {
- if (paymentTransactionId != null && input.getId().equals(paymentTransactionId)) {
- return true;
- }
- if (paymentTransactionExternalKey != null && input.getTransactionExternalKey().equals(paymentTransactionExternalKey)) {
- return true;
- }
- return false;
- }
- });
- }
-
- protected Iterable<PaymentTransactionModelDao> filterPendingTransactionsForPaymentAndTransactionType(final Iterable<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment, final TransactionType transactionType) throws PaymentApiException {
- return Iterables.filter(paymentTransactionsForCurrentPayment, new Predicate<PaymentTransactionModelDao>() {
- @Override
- public boolean apply(final PaymentTransactionModelDao input) {
- return input.getTransactionStatus() == TransactionStatus.PENDING &&
- input.getTransactionType() == transactionType;
- }
- });
- }
-
- protected PaymentTransactionModelDao filterPendingTransactionsForTransactionKey(final Iterable<PaymentTransactionModelDao> existingPendingPaymentTransactions, final String paymentTransactionExternalKey) throws PaymentApiException {
- return Iterables.tryFind(existingPendingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
- @Override
- public boolean apply(final PaymentTransactionModelDao input) {
- return input.getTransactionExternalKey().equals(paymentTransactionExternalKey);
- }
- }).orNull();
- }
-
- protected void validateUniqueTransactionExternalKey(final Iterable<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
- // If no key specified, system will allocate a unique one later, there is nothing to check
- if (paymentStateContext.getPaymentTransactionExternalKey() == null) {
- return;
- }
-
- if (Iterables.any(existingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
- @Override
- public boolean apply(final PaymentTransactionModelDao input) {
- // An existing transaction in a SUCCESS state
- return input.getTransactionStatus() == TransactionStatus.SUCCESS ||
- // Or, an existing transaction for a different payment (to do really well, we should also check on paymentExternalKey which is not available here)
- (paymentStateContext.getPaymentId() != null && input.getPaymentId().compareTo(paymentStateContext.getPaymentId()) != 0) ||
- // Or, an existing transaction for a different account.
- (!input.getAccountRecordId().equals(paymentStateContext.getInternalCallContext().getAccountRecordId()));
-
- }
- })) {
- throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
- }
- }
-
- // At this point, the payment id should have been populated for follow-up transactions (see PaymentAutomationRunner#run)
- protected void validatePaymentIdAndTransactionType(final Iterable<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
- for (final PaymentTransactionModelDao paymentTransactionModelDao : existingPaymentTransactions) {
- if (!paymentTransactionModelDao.getPaymentId().equals(paymentStateContext.getPaymentId())) {
- throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, paymentTransactionModelDao.getId(), "does not belong to payment " + paymentStateContext.getPaymentId());
- }
- if (paymentStateContext.getTransactionType() != null && paymentTransactionModelDao.getTransactionType() != paymentStateContext.getTransactionType()) {
- throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, paymentTransactionModelDao.getId(), "has a transaction type of " + paymentTransactionModelDao.getTransactionType() +
- " instead of requested " + paymentStateContext.getTransactionType());
- }
- }
- }
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java
index 433ed5b..d6a1e7d 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java
@@ -71,7 +71,7 @@ public abstract class PaymentOperation extends OperationCallbackBase<PaymentTran
final String pluginName;
try {
pluginName = daoHelper.getPaymentProviderPluginName();
- this.plugin = daoHelper.getPaymentPluginApi(pluginName);
+ this.plugin = daoHelper.getPaymentPluginApi();
} catch (final PaymentApiException e) {
throw convertToUnknownTransactionStatusAndErroredPaymentState(e);
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java
index dd4f298..5ed95a9 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java
@@ -21,17 +21,12 @@ import javax.inject.Inject;
import org.killbill.automaton.MissingEntryException;
import org.killbill.automaton.Operation;
-import org.killbill.automaton.OperationResult;
-import org.killbill.automaton.State;
import org.killbill.automaton.StateMachine;
import org.killbill.automaton.StateMachineConfig;
-import org.killbill.automaton.Transition;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.TransactionType;
-import org.killbill.billing.payment.glue.PaymentModule;
-
-import com.google.common.base.Predicate;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
+import org.killbill.billing.payment.caching.StateMachineConfigCache;
/**
* This class needs to know about the payment state machine xml file. All the knowledge about the xml file is encapsulated here.
@@ -80,17 +75,12 @@ public class PaymentStateMachineHelper {
private static final String CREDIT_ERRORED = "CREDIT_ERRORED";
private static final String VOID_ERRORED = "VOID_ERRORED";
private static final String CHARGEBACK_ERRORED = "CHARGEBACK_ERRORED";
- private final StateMachineConfig stateMachineConfig;
- private final String[] errorStateNames = {AUTH_ERRORED, CAPTURE_ERRORED, PURCHASE_ERRORED, REFUND_ERRORED, CREDIT_ERRORED, VOID_ERRORED, CHARGEBACK_ERRORED};
- @Inject
- public PaymentStateMachineHelper(@javax.inject.Named(PaymentModule.STATE_MACHINE_PAYMENT) final StateMachineConfig stateMachineConfig) {
- this.stateMachineConfig = stateMachineConfig;
- }
+ private final StateMachineConfigCache stateMachineConfigCache;
- public State getState(final String stateName) throws MissingEntryException {
- final StateMachine stateMachine = stateMachineConfig.getStateMachineForState(stateName);
- return stateMachine.getState(stateName);
+ @Inject
+ public PaymentStateMachineHelper(final StateMachineConfigCache stateMachineConfigCache) {
+ this.stateMachineConfigCache = stateMachineConfigCache;
}
public String getInitStateNameForTransaction() {
@@ -181,17 +171,17 @@ public class PaymentStateMachineHelper {
}
}
- public StateMachine getStateMachineForStateName(final String stateName) throws MissingEntryException {
- return stateMachineConfig.getStateMachineForState(stateName);
+ public StateMachineConfig getStateMachineConfig(final String pluginName, final InternalCallContext internalCallContext) throws PaymentApiException {
+ return stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, internalCallContext);
}
- public Operation getOperationForTransaction(final TransactionType transactionType) throws MissingEntryException {
- final StateMachine stateMachine = getStateMachineForTransaction(transactionType);
+ public Operation getOperationForTransaction(final StateMachineConfig stateMachineConfig, final TransactionType transactionType) throws MissingEntryException {
+ final StateMachine stateMachine = getStateMachineForTransaction(stateMachineConfig, transactionType);
// Only one operation defined, this is the current PaymentStates.xml model
return stateMachine.getOperations()[0];
}
- public StateMachine getStateMachineForTransaction(final TransactionType transactionType) throws MissingEntryException {
+ private StateMachine getStateMachineForTransaction(final StateMachineConfig stateMachineConfig, final TransactionType transactionType) throws MissingEntryException {
switch (transactionType) {
case AUTHORIZE:
return stateMachineConfig.getStateMachine(AUTHORIZE_STATE_MACHINE_NAME);
@@ -216,22 +206,4 @@ public class PaymentStateMachineHelper {
public boolean isSuccessState(final String stateName) {
return stateName.endsWith("SUCCESS") || stateName.startsWith("CHARGEBACK");
}
-
- public final State fetchNextState(final String prevStateName, final boolean isSuccess) throws MissingEntryException {
- final StateMachine stateMachine = getStateMachineForStateName(prevStateName);
- final Transition transition = Iterables.tryFind(ImmutableList.copyOf(stateMachine.getTransitions()), new Predicate<Transition>() {
- @Override
- public boolean apply(final Transition input) {
- // This works because there is only one operation defined for a given state machine, which is our model for PaymentStates.xml
- return input.getInitialState().getName().equals(prevStateName) &&
- input.getOperationResult().equals(isSuccess ? OperationResult.SUCCESS : OperationResult.FAILURE);
- }
- }).orNull();
- return transition != null ? transition.getFinalState() : null;
- }
-
- public String[] getErroredStateNames() {
- return errorStateNames;
- }
-
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
index 3be74e7..0e67704 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
@@ -297,7 +297,7 @@ public class DefaultPaymentDao implements PaymentDao {
}
@Override
- public void updatePaymentAndTransactionOnCompletion(final UUID accountId, final UUID paymentId, final TransactionType transactionType,
+ public void updatePaymentAndTransactionOnCompletion(final UUID accountId, @Nullable final UUID attemptId, final UUID paymentId, final TransactionType transactionType,
final String currentPaymentStateName, @Nullable final String lastPaymentSuccessStateName,
final UUID transactionId, final TransactionStatus transactionStatus,
final BigDecimal processedAmount, final Currency processedCurrency,
@@ -308,10 +308,16 @@ public class DefaultPaymentDao implements PaymentDao {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InternalCallContext contextWithUpdatedDate = contextWithUpdatedDate(context);
- entitySqlDaoWrapperFactory.become(TransactionSqlDao.class).updateTransactionStatus(transactionId.toString(),
- processedAmount, processedCurrency == null ? null : processedCurrency.toString(),
- transactionStatus == null ? null : transactionStatus.toString(),
- gatewayErrorCode, gatewayErrorMsg, contextWithUpdatedDate);
+ final TransactionSqlDao transactional = entitySqlDaoWrapperFactory.become(TransactionSqlDao.class);
+ final PaymentTransactionModelDao paymentTransactionModelDao = transactional.getById(transactionId.toString(), context);
+ transactional.updateTransactionStatus(transactionId.toString(),
+ attemptId == null ? (paymentTransactionModelDao.getAttemptId() == null ? null : paymentTransactionModelDao.getAttemptId().toString()) : attemptId.toString(),
+ processedAmount,
+ processedCurrency == null ? null : processedCurrency.toString(),
+ transactionStatus == null ? null : transactionStatus.toString(),
+ gatewayErrorCode,
+ gatewayErrorMsg,
+ contextWithUpdatedDate);
if (lastPaymentSuccessStateName != null) {
entitySqlDaoWrapperFactory.become(PaymentSqlDao.class).updateLastSuccessPaymentStateName(paymentId.toString(), currentPaymentStateName, lastPaymentSuccessStateName, contextWithUpdatedDate);
} else {
@@ -321,7 +327,6 @@ public class DefaultPaymentDao implements PaymentDao {
return null;
}
});
-
}
@Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
index 4c3eeee..dc1f8f7 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
@@ -56,7 +56,7 @@ public interface PaymentDao {
public PaymentTransactionModelDao updatePaymentWithNewTransaction(UUID paymentId, PaymentTransactionModelDao paymentTransaction, InternalCallContext context);
- public void updatePaymentAndTransactionOnCompletion(UUID accountId, UUID paymentId, TransactionType transactionType, String currentPaymentStateName, String lastPaymentSuccessStateName, UUID transactionId,
+ public void updatePaymentAndTransactionOnCompletion(UUID accountId, UUID attemptId, UUID paymentId, TransactionType transactionType, String currentPaymentStateName, String lastPaymentSuccessStateName, UUID transactionId,
TransactionStatus paymentStatus, BigDecimal processedAmount, Currency processedCurrency,
String gatewayErrorCode, String gatewayErrorMsg, InternalCallContext context);
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/TransactionSqlDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/TransactionSqlDao.java
index d31d589..d85ac3a 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/TransactionSqlDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/TransactionSqlDao.java
@@ -1,5 +1,6 @@
/*
- * Copyright 2014 Groupon, Inc
+ * 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
@@ -41,6 +42,7 @@ public interface TransactionSqlDao extends EntitySqlDao<PaymentTransactionModelD
@SqlUpdate
@Audited(ChangeType.UPDATE)
void updateTransactionStatus(@Bind("id") final String transactionId,
+ @Bind("attemptId") final String attemptId,
@Bind("processedAmount") final BigDecimal processedAmount,
@Bind("processedCurrency") final String processedCurrency,
@Bind("transactionStatus") final String transactionStatus,
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java
index e27a8b2..c0180be 100644
--- a/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java
@@ -1,7 +1,7 @@
/*
- * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2010-2014 Ning, Inc.
+ * 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
@@ -19,11 +19,13 @@
package org.killbill.billing.payment.glue;
import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.PaymentService;
import org.killbill.billing.payment.bus.PaymentBusEventHandler;
+import org.killbill.billing.payment.caching.StateMachineConfigCache;
import org.killbill.billing.payment.core.PaymentExecutors;
-import org.killbill.billing.payment.invoice.PaymentTagHandler;
import org.killbill.billing.payment.core.janitor.Janitor;
+import org.killbill.billing.payment.invoice.PaymentTagHandler;
import org.killbill.billing.payment.retry.DefaultRetryService;
import org.killbill.billing.platform.api.LifecycleHandlerType;
import org.killbill.billing.platform.api.LifecycleHandlerType.LifecycleLevel;
@@ -48,6 +50,7 @@ public class DefaultPaymentService implements PaymentService {
private final DefaultRetryService retryService;
private final Janitor janitor;
private final PaymentExecutors paymentExecutors;
+ private final StateMachineConfigCache stateMachineConfigCache;
@Inject
public DefaultPaymentService(final PaymentBusEventHandler paymentBusEventHandler,
@@ -56,7 +59,8 @@ public class DefaultPaymentService implements PaymentService {
final DefaultRetryService retryService,
final PersistentBus eventBus,
final Janitor janitor,
- final PaymentExecutors paymentExecutors) {
+ final PaymentExecutors paymentExecutors,
+ final StateMachineConfigCache stateMachineConfigCache) {
this.paymentBusEventHandler = paymentBusEventHandler;
this.tagHandler = tagHandler;
this.eventBus = eventBus;
@@ -64,6 +68,7 @@ public class DefaultPaymentService implements PaymentService {
this.retryService = retryService;
this.janitor = janitor;
this.paymentExecutors = paymentExecutors;
+ this.stateMachineConfigCache = stateMachineConfigCache;
}
@Override
@@ -74,6 +79,12 @@ public class DefaultPaymentService implements PaymentService {
@LifecycleHandlerType(LifecycleLevel.INIT_SERVICE)
public void initialize() throws NotificationQueueAlreadyExists {
try {
+ stateMachineConfigCache.loadDefaultPaymentStateMachineConfig(PaymentModule.DEFAULT_STATE_MACHINE_PAYMENT_XML);
+ } catch (final PaymentApiException e) {
+ log.error("Unable to load default payment state machine");
+ }
+
+ try {
eventBus.register(paymentBusEventHandler);
eventBus.register(tagHandler);
} catch (final PersistentBus.EventBusException e) {
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
index fc0e8f7..9b066d3 100644
--- a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
@@ -1,7 +1,7 @@
/*
- * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2010-2014 Ning, Inc.
+ * 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
@@ -33,6 +33,9 @@ import org.killbill.billing.payment.api.PaymentGatewayApi;
import org.killbill.billing.payment.api.PaymentService;
import org.killbill.billing.payment.bus.PaymentBusEventHandler;
import org.killbill.billing.payment.config.MultiTenantPaymentConfig;
+import org.killbill.billing.payment.caching.EhCacheStateMachineConfigCache;
+import org.killbill.billing.payment.caching.StateMachineConfigCache;
+import org.killbill.billing.payment.caching.StateMachineConfigCacheInvalidationCallback;
import org.killbill.billing.payment.core.PaymentExecutors;
import org.killbill.billing.payment.core.PaymentGatewayProcessor;
import org.killbill.billing.payment.core.PaymentMethodProcessor;
@@ -54,6 +57,7 @@ import org.killbill.billing.payment.retry.DefaultRetryService.DefaultRetryServic
import org.killbill.billing.payment.retry.RetryService;
import org.killbill.billing.platform.api.KillbillConfigSource;
import org.killbill.billing.util.config.definition.PaymentConfig;
+import org.killbill.billing.tenant.api.TenantInternalApi.CacheInvalidationCallback;
import org.killbill.billing.util.glue.KillBillModule;
import org.killbill.xmlloader.XMLLoader;
import org.skife.config.ConfigurationObjectFactory;
@@ -71,12 +75,13 @@ public class PaymentModule extends KillBillModule {
public static final String RETRYABLE_NAMED = "Retryable";
public static final String STATE_MACHINE_RETRY = "RetryStateMachine";
- public static final String STATE_MACHINE_PAYMENT = "PaymentStateMachine";
@VisibleForTesting
- static final String DEFAULT_STATE_MACHINE_RETRY_XML = "org/killbill/billing/payment/retry/RetryStates.xml";
+ public static final String DEFAULT_STATE_MACHINE_RETRY_XML = "org/killbill/billing/payment/retry/RetryStates.xml";
@VisibleForTesting
- static final String DEFAULT_STATE_MACHINE_PAYMENT_XML = "org/killbill/billing/payment/PaymentStates.xml";
+ public static final String DEFAULT_STATE_MACHINE_PAYMENT_XML = "org/killbill/billing/payment/PaymentStates.xml";
+
+ public static final String STATE_MACHINE_CONFIG_INVALIDATION_CALLBACK = "StateMachineConfigInvalidationCallback";
public PaymentModule(final KillbillConfigSource configSource) {
super(configSource);
@@ -104,13 +109,14 @@ public class PaymentModule extends KillBillModule {
}
protected void installStateMachines() {
-
bind(StateMachineProvider.class).annotatedWith(Names.named(STATE_MACHINE_RETRY)).toInstance(new StateMachineProvider(DEFAULT_STATE_MACHINE_RETRY_XML));
bind(StateMachineConfig.class).annotatedWith(Names.named(STATE_MACHINE_RETRY)).toProvider(Key.get(StateMachineProvider.class, Names.named(STATE_MACHINE_RETRY)));
+
bind(PaymentControlStateMachineHelper.class).asEagerSingleton();
- bind(StateMachineProvider.class).annotatedWith(Names.named(STATE_MACHINE_PAYMENT)).toInstance(new StateMachineProvider(DEFAULT_STATE_MACHINE_PAYMENT_XML));
- bind(StateMachineConfig.class).annotatedWith(Names.named(STATE_MACHINE_PAYMENT)).toProvider(Key.get(StateMachineProvider.class, Names.named(STATE_MACHINE_PAYMENT)));
+ bind(StateMachineConfigCache.class).to(EhCacheStateMachineConfigCache.class).asEagerSingleton();
+ bind(CacheInvalidationCallback.class).annotatedWith(Names.named(STATE_MACHINE_CONFIG_INVALIDATION_CALLBACK)).to(StateMachineConfigCacheInvalidationCallback.class).asEagerSingleton();
+
bind(PaymentStateMachineHelper.class).asEagerSingleton();
bind(ControlPluginRunner.class).asEagerSingleton();
diff --git a/payment/src/main/resources/org/killbill/billing/payment/dao/TransactionSqlDao.sql.stg b/payment/src/main/resources/org/killbill/billing/payment/dao/TransactionSqlDao.sql.stg
index 3a91dc9..c00aee0 100644
--- a/payment/src/main/resources/org/killbill/billing/payment/dao/TransactionSqlDao.sql.stg
+++ b/payment/src/main/resources/org/killbill/billing/payment/dao/TransactionSqlDao.sql.stg
@@ -60,6 +60,7 @@ where transaction_external_key = :transactionExternalKey
updateTransactionStatus() ::= <<
update <tableName()>
set transaction_status = :transactionStatus
+, attempt_id = :attemptId
, processed_amount = :processedAmount
, processed_currency = :processedCurrency
, gateway_error_code = :gatewayErrorCode
diff --git a/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml b/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
index da851a2..9031c87 100644
--- a/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
+++ b/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
@@ -441,30 +441,6 @@
<finalState>VOID_INIT</finalState>
</linkStateMachine>
<linkStateMachine>
- <initialStateMachine>VOID</initialStateMachine>
- <initialState>VOID_SUCCESS</initialState>
- <finalStateMachine>VOID</finalStateMachine>
- <finalState>VOID_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
- <initialStateMachine>VOID</initialStateMachine>
- <initialState>VOID_SUCCESS</initialState>
- <finalStateMachine>CAPTURE</finalStateMachine>
- <finalState>CAPTURE_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
- <initialStateMachine>VOID</initialStateMachine>
- <initialState>VOID_SUCCESS</initialState>
- <finalStateMachine>REFUND</finalStateMachine>
- <finalState>REFUND_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
- <initialStateMachine>VOID</initialStateMachine>
- <initialState>VOID_SUCCESS</initialState>
- <finalStateMachine>CREDIT</finalStateMachine>
- <finalState>CREDIT_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
<initialStateMachine>CAPTURE</initialStateMachine>
<initialState>CAPTURE_SUCCESS</initialState>
<finalStateMachine>REFUND</finalStateMachine>
@@ -483,12 +459,6 @@
<finalState>CHARGEBACK_INIT</finalState>
</linkStateMachine>
<linkStateMachine>
- <initialStateMachine>CAPTURE</initialStateMachine>
- <initialState>CAPTURE_SUCCESS</initialState>
- <finalStateMachine>VOID</finalStateMachine>
- <finalState>VOID_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
<initialStateMachine>REFUND</initialStateMachine>
<initialState>REFUND_SUCCESS</initialState>
<finalStateMachine>REFUND</finalStateMachine>
@@ -501,12 +471,6 @@
<finalState>CHARGEBACK_INIT</finalState>
</linkStateMachine>
<linkStateMachine>
- <initialStateMachine>REFUND</initialStateMachine>
- <initialState>REFUND_SUCCESS</initialState>
- <finalStateMachine>VOID</finalStateMachine>
- <finalState>VOID_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
<initialStateMachine>PURCHASE</initialStateMachine>
<initialState>PURCHASE_SUCCESS</initialState>
<finalStateMachine>REFUND</finalStateMachine>
@@ -519,12 +483,6 @@
<finalState>CHARGEBACK_INIT</finalState>
</linkStateMachine>
<linkStateMachine>
- <initialStateMachine>CREDIT</initialStateMachine>
- <initialState>CREDIT_SUCCESS</initialState>
- <finalStateMachine>VOID</finalStateMachine>
- <finalState>VOID_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
<initialStateMachine>CHARGEBACK</initialStateMachine>
<initialState>CHARGEBACK_SUCCESS</initialState>
<finalStateMachine>CHARGEBACK</finalStateMachine>
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultAdminPaymentApi.java b/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultAdminPaymentApi.java
new file mode 100644
index 0000000..03f6d8a
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultAdminPaymentApi.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 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.payment.api;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.control.plugin.api.PaymentControlPluginApi;
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
+import org.killbill.billing.payment.dao.PaymentModelDao;
+import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin;
+import org.killbill.billing.payment.provider.DefaultNoOpPaymentInfoPlugin;
+import org.killbill.billing.payment.provider.MockPaymentControlProviderPlugin;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestDefaultAdminPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
+
+ private MockPaymentProviderPlugin mockPaymentProviderPlugin;
+ private Account account;
+
+ @BeforeClass(groups = "slow")
+ protected void beforeClass() throws Exception {
+ super.beforeClass();
+ mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(MockPaymentProviderPlugin.PLUGIN_NAME);
+ }
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+
+ mockPaymentProviderPlugin.clear();
+ account = testHelper.createTestAccount("bobo@gmail.com", true);
+
+ final PaymentControlPluginApi mockPaymentControlProviderPlugin = new MockPaymentControlProviderPlugin();
+ controlPluginRegistry.registerService(new OSGIServiceDescriptor() {
+ @Override
+ public String getPluginSymbolicName() {
+ return null;
+ }
+
+ @Override
+ public String getPluginName() {
+ return MockPaymentControlProviderPlugin.PLUGIN_NAME;
+ }
+
+ @Override
+ public String getRegistrationName() {
+ return MockPaymentControlProviderPlugin.PLUGIN_NAME;
+ }
+ },
+ mockPaymentControlProviderPlugin);
+ }
+
+ @Test(groups = "slow")
+ public void testFixPaymentTransactionState() throws PaymentApiException {
+ final Payment payment = paymentApi.createAuthorization(account,
+ account.getPaymentMethodId(),
+ null,
+ BigDecimal.TEN,
+ Currency.EUR,
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(),
+ callContext);
+
+ final PaymentModelDao paymentModelDao = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(payment.getTransactions().get(0).getId(), internalCallContext);
+ Assert.assertEquals(paymentModelDao.getStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(paymentModelDao.getLastSuccessStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(paymentTransactionModelDao.getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(paymentTransactionModelDao.getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(paymentTransactionModelDao.getProcessedCurrency(), Currency.EUR);
+ Assert.assertEquals(paymentTransactionModelDao.getGatewayErrorCode(), "");
+ Assert.assertEquals(paymentTransactionModelDao.getGatewayErrorMsg(), "");
+
+ adminPaymentApi.fixPaymentTransactionState(payment, payment.getTransactions().get(0), TransactionStatus.PAYMENT_FAILURE, null, "AUTH_ERRORED", ImmutableList.<PluginProperty>of(), callContext);
+
+ final PaymentModelDao refreshedPaymentModelDao = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao refreshedPaymentTransactionModelDao = paymentDao.getPaymentTransaction(payment.getTransactions().get(0).getId(), internalCallContext);
+ Assert.assertEquals(refreshedPaymentModelDao.getStateName(), "AUTH_ERRORED");
+ // TODO Shouldn't we allow the user to override this too?
+ Assert.assertEquals(refreshedPaymentModelDao.getLastSuccessStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getProcessedCurrency(), Currency.EUR);
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getGatewayErrorCode(), "");
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getGatewayErrorMsg(), "");
+ }
+
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/551")
+ public void testFixPaymentTransactionStateNoPaymentTransactionInfoPlugin() throws PaymentApiException {
+ final Payment payment = paymentApi.createAuthorization(account,
+ account.getPaymentMethodId(),
+ null,
+ BigDecimal.TEN,
+ Currency.EUR,
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(),
+ callContext);
+
+ final PaymentModelDao paymentModelDao = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(payment.getTransactions().get(0).getId(), internalCallContext);
+ Assert.assertEquals(paymentModelDao.getStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(paymentModelDao.getLastSuccessStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(paymentTransactionModelDao.getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(paymentTransactionModelDao.getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(paymentTransactionModelDao.getProcessedCurrency(), Currency.EUR);
+ Assert.assertEquals(paymentTransactionModelDao.getGatewayErrorCode(), "");
+ Assert.assertEquals(paymentTransactionModelDao.getGatewayErrorMsg(), "");
+
+ try {
+ // Since no transaction status is passed, PaymentTransactionInfoPlugin should be set
+ adminPaymentApi.fixPaymentTransactionState(payment, Mockito.mock(DefaultPaymentTransaction.class), null, null, "AUTH_ERRORED", ImmutableList.<PluginProperty>of(), callContext);
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
+ }
+ }
+
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/551")
+ public void testFixPaymentTransactionStateFromPaymentTransactionInfoPlugin() throws PaymentApiException {
+ final Payment payment = paymentApi.createAuthorization(account,
+ account.getPaymentMethodId(),
+ null,
+ BigDecimal.TEN,
+ Currency.EUR,
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(),
+ callContext);
+
+ final PaymentModelDao paymentModelDao = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(payment.getTransactions().get(0).getId(), internalCallContext);
+ Assert.assertEquals(paymentModelDao.getStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(paymentModelDao.getLastSuccessStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(paymentTransactionModelDao.getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(paymentTransactionModelDao.getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(paymentTransactionModelDao.getProcessedCurrency(), Currency.EUR);
+ Assert.assertEquals(paymentTransactionModelDao.getGatewayErrorCode(), "");
+ Assert.assertEquals(paymentTransactionModelDao.getGatewayErrorMsg(), "");
+
+ final PaymentTransactionInfoPlugin infoPlugin = new DefaultNoOpPaymentInfoPlugin(paymentTransactionModelDao.getPaymentId(),
+ paymentTransactionModelDao.getId(),
+ paymentTransactionModelDao.getTransactionType(),
+ paymentTransactionModelDao.getAmount(),
+ paymentTransactionModelDao.getCurrency(),
+ paymentTransactionModelDao.getEffectiveDate(),
+ paymentTransactionModelDao.getCreatedDate(),
+ PaymentPluginStatus.ERROR,
+ "error-code",
+ "error-msg");
+ final PaymentTransaction newPaymentTransaction = new DefaultPaymentTransaction(paymentTransactionModelDao.getId(),
+ paymentTransactionModelDao.getAttemptId(),
+ paymentTransactionModelDao.getTransactionExternalKey(),
+ paymentTransactionModelDao.getCreatedDate(),
+ paymentTransactionModelDao.getUpdatedDate(),
+ paymentTransactionModelDao.getPaymentId(),
+ paymentTransactionModelDao.getTransactionType(),
+ paymentTransactionModelDao.getEffectiveDate(),
+ TransactionStatus.PAYMENT_FAILURE,
+ paymentTransactionModelDao.getAmount(),
+ paymentTransactionModelDao.getCurrency(),
+ paymentTransactionModelDao.getProcessedAmount(),
+ paymentTransactionModelDao.getProcessedCurrency(),
+ infoPlugin.getGatewayErrorCode(),
+ infoPlugin.getGatewayError(),
+ infoPlugin);
+ adminPaymentApi.fixPaymentTransactionState(payment, newPaymentTransaction, null, null, "AUTH_ERRORED", ImmutableList.<PluginProperty>of(), callContext);
+
+ final PaymentModelDao refreshedPaymentModelDao = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao refreshedPaymentTransactionModelDao = paymentDao.getPaymentTransaction(payment.getTransactions().get(0).getId(), internalCallContext);
+ Assert.assertEquals(refreshedPaymentModelDao.getStateName(), "AUTH_ERRORED");
+ // TODO Shouldn't we allow the user to override this too?
+ Assert.assertEquals(refreshedPaymentModelDao.getLastSuccessStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getProcessedCurrency(), Currency.EUR);
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getGatewayErrorCode(), "error-code");
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getGatewayErrorMsg(), "error-msg");
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultPayment.java b/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultPayment.java
index 7cf7830..6bf1743 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultPayment.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultPayment.java
@@ -33,7 +33,6 @@ public class TestDefaultPayment extends PaymentTestSuiteNoDB {
@Test(groups = "fast")
public void testAmountsCaptureVoided() throws Exception {
final UUID paymentId = UUID.randomUUID();
- final String chargebackExternalKey = UUID.randomUUID().toString();
final List<PaymentTransaction> transactions = ImmutableList.<PaymentTransaction>of(buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.AUTHORIZE, TransactionStatus.SUCCESS, BigDecimal.TEN),
buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.CAPTURE, TransactionStatus.SUCCESS, BigDecimal.TEN),
buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.VOID, TransactionStatus.SUCCESS, null));
@@ -47,7 +46,6 @@ public class TestDefaultPayment extends PaymentTestSuiteNoDB {
@Test(groups = "fast")
public void testAmountsCaptureVoidedAuthReversed() throws Exception {
final UUID paymentId = UUID.randomUUID();
- final String chargebackExternalKey = UUID.randomUUID().toString();
final List<PaymentTransaction> transactions = ImmutableList.<PaymentTransaction>of(buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.AUTHORIZE, TransactionStatus.SUCCESS, BigDecimal.TEN),
buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.CAPTURE, TransactionStatus.SUCCESS, BigDecimal.TEN),
buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.VOID, TransactionStatus.SUCCESS, null),
@@ -89,6 +87,22 @@ public class TestDefaultPayment extends PaymentTestSuiteNoDB {
}
@Test(groups = "fast")
+ public void testAmountsCaptureChargebackReversedMultipleCurrencies() throws Exception {
+ final UUID paymentId = UUID.randomUUID();
+ final String chargebackExternalKey = UUID.randomUUID().toString();
+ final List<PaymentTransaction> transactions = ImmutableList.<PaymentTransaction>of(buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.AUTHORIZE, TransactionStatus.SUCCESS, BigDecimal.TEN, Currency.EUR),
+ buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.CAPTURE, TransactionStatus.SUCCESS, BigDecimal.TEN, Currency.USD),
+ buildPaymentTransaction(paymentId, chargebackExternalKey, TransactionType.CHARGEBACK, TransactionStatus.SUCCESS, BigDecimal.ONE, Currency.EUR),
+ buildPaymentTransaction(paymentId, chargebackExternalKey, TransactionType.CHARGEBACK, TransactionStatus.PAYMENT_FAILURE, BigDecimal.ONE, Currency.EUR));
+ final Payment payment = buildPayment(paymentId, transactions);
+ Assert.assertEquals(payment.getCurrency(), Currency.EUR);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ }
+
+ @Test(groups = "fast")
public void testAmountsCaptureChargebackReversedAndRefund() throws Exception {
final UUID paymentId = UUID.randomUUID();
final String chargebackExternalKey = UUID.randomUUID().toString();
@@ -118,6 +132,19 @@ public class TestDefaultPayment extends PaymentTestSuiteNoDB {
}
@Test(groups = "fast")
+ public void testAmountsPurchaseChargebackDifferentCurrency() throws Exception {
+ final UUID paymentId = UUID.randomUUID();
+ final String chargebackExternalKey = UUID.randomUUID().toString();
+ final List<PaymentTransaction> transactions = ImmutableList.<PaymentTransaction>of(buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.PURCHASE, TransactionStatus.SUCCESS, BigDecimal.TEN, Currency.USD),
+ buildPaymentTransaction(paymentId, chargebackExternalKey, TransactionType.CHARGEBACK, TransactionStatus.SUCCESS, BigDecimal.ONE, Currency.EUR));
+ final Payment payment = buildPayment(paymentId, transactions);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ }
+
+ @Test(groups = "fast")
public void testAmountsPurchaseChargebackReversed() throws Exception {
final UUID paymentId = UUID.randomUUID();
final String chargebackExternalKey = UUID.randomUUID().toString();
@@ -163,6 +190,10 @@ public class TestDefaultPayment extends PaymentTestSuiteNoDB {
}
private PaymentTransaction buildPaymentTransaction(final UUID paymentId, final String externalKey, final TransactionType transactionType, final TransactionStatus transactionStatus, final BigDecimal amount) {
+ return buildPaymentTransaction(paymentId, externalKey, transactionType, transactionStatus, amount, Currency.USD);
+ }
+
+ private PaymentTransaction buildPaymentTransaction(final UUID paymentId, final String externalKey, final TransactionType transactionType, final TransactionStatus transactionStatus, final BigDecimal amount, final Currency currency) {
return new DefaultPaymentTransaction(UUID.randomUUID(),
UUID.randomUUID(),
externalKey,
@@ -173,9 +204,9 @@ public class TestDefaultPayment extends PaymentTestSuiteNoDB {
clock.getUTCNow(),
transactionStatus,
amount,
- Currency.USD,
+ currency,
amount,
- Currency.USD,
+ currency,
null,
null,
null);
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
index 78437bc..d455610 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
@@ -412,10 +412,21 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(payment2.getTransactions().get(1).getTransactionType(), TransactionType.VOID);
assertNotNull(payment2.getTransactions().get(1).getGatewayErrorMsg());
assertNotNull(payment2.getTransactions().get(1).getGatewayErrorCode());
+
+ try {
+ // Verify further VOIDs are prohibited (see https://github.com/killbill/killbill/issues/514)
+ paymentApi.createVoid(account, payment.getId(), UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
}
@Test(groups = "slow")
public void testCreateSuccessAuthCaptureVoidCapture() throws PaymentApiException {
+ // Overwrite the default state machine to allow void on captures
+ stateMachineConfigCache.loadDefaultPaymentStateMachineConfig("org/killbill/billing/payment/PermissivePaymentStates.xml");
+
final BigDecimal authAmount = BigDecimal.TEN;
final BigDecimal captureAmount = BigDecimal.ONE;
@@ -533,7 +544,82 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
+ public void testCreateSuccessAuthCaptureVoidFailed() throws PaymentApiException {
+ final BigDecimal authAmount = BigDecimal.TEN;
+ final BigDecimal captureAmount = BigDecimal.ONE;
+
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final String transactionExternalKey = UUID.randomUUID().toString();
+ final String transactionExternalKey2 = UUID.randomUUID().toString();
+ final String transactionExternalKey3 = UUID.randomUUID().toString();
+
+ final Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, authAmount, Currency.AED,
+ paymentExternalKey, transactionExternalKey,
+ ImmutableList.<PluginProperty>of(), callContext);
+
+ assertEquals(payment.getExternalKey(), paymentExternalKey);
+ assertEquals(payment.getPaymentMethodId(), account.getPaymentMethodId());
+ assertEquals(payment.getAccountId(), account.getId());
+ assertEquals(payment.getAuthAmount().compareTo(authAmount), 0);
+ assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment.getCurrency(), Currency.AED);
+ assertFalse(payment.isAuthVoided());
+
+ assertEquals(payment.getTransactions().size(), 1);
+ assertEquals(payment.getTransactions().get(0).getExternalKey(), transactionExternalKey);
+ assertEquals(payment.getTransactions().get(0).getPaymentId(), payment.getId());
+ assertEquals(payment.getTransactions().get(0).getAmount().compareTo(authAmount), 0);
+ assertEquals(payment.getTransactions().get(0).getCurrency(), Currency.AED);
+ assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(authAmount), 0);
+ assertEquals(payment.getTransactions().get(0).getProcessedCurrency(), Currency.AED);
+
+ assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payment.getTransactions().get(0).getTransactionType(), TransactionType.AUTHORIZE);
+ assertNotNull(payment.getTransactions().get(0).getGatewayErrorMsg());
+ assertNotNull(payment.getTransactions().get(0).getGatewayErrorCode());
+
+ final Payment payment2 = paymentApi.createCapture(account, payment.getId(), captureAmount, Currency.AED, transactionExternalKey2,
+ ImmutableList.<PluginProperty>of(), callContext);
+
+ assertEquals(payment2.getExternalKey(), paymentExternalKey);
+ assertEquals(payment2.getPaymentMethodId(), account.getPaymentMethodId());
+ assertEquals(payment2.getAccountId(), account.getId());
+ assertEquals(payment2.getAuthAmount().compareTo(authAmount), 0);
+ assertEquals(payment2.getCapturedAmount().compareTo(captureAmount), 0);
+ assertEquals(payment2.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment2.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment2.getCurrency(), Currency.AED);
+ assertFalse(payment2.isAuthVoided());
+
+ assertEquals(payment2.getTransactions().size(), 2);
+ assertEquals(payment2.getTransactions().get(1).getExternalKey(), transactionExternalKey2);
+ assertEquals(payment2.getTransactions().get(1).getPaymentId(), payment.getId());
+ assertEquals(payment2.getTransactions().get(1).getAmount().compareTo(captureAmount), 0);
+ assertEquals(payment2.getTransactions().get(1).getCurrency(), Currency.AED);
+ assertEquals(payment2.getTransactions().get(1).getProcessedAmount().compareTo(captureAmount), 0);
+ assertEquals(payment2.getTransactions().get(1).getProcessedCurrency(), Currency.AED);
+
+ assertEquals(payment2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payment2.getTransactions().get(1).getTransactionType(), TransactionType.CAPTURE);
+ assertNotNull(payment2.getTransactions().get(1).getGatewayErrorMsg());
+ assertNotNull(payment2.getTransactions().get(1).getGatewayErrorCode());
+
+ try {
+ // Voiding a capture is prohibited by default
+ paymentApi.createVoid(account, payment.getId(), transactionExternalKey3, ImmutableList.<PluginProperty>of(), callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
public void testCreateSuccessAuthCaptureVoidVoid() throws PaymentApiException {
+ // Overwrite the default state machine to allow void on captures
+ stateMachineConfigCache.loadDefaultPaymentStateMachineConfig("org/killbill/billing/payment/PermissivePaymentStates.xml");
+
final BigDecimal authAmount = BigDecimal.TEN;
final BigDecimal captureAmount = BigDecimal.ONE;
@@ -1341,6 +1427,42 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
+ public void testVerifyJanitorFromPendingDuringCompletionFlow() throws PaymentApiException {
+ final BigDecimal authAmount = BigDecimal.TEN;
+ final String transactionExternalKey = UUID.randomUUID().toString();
+
+ final Payment initialPayment = createPayment(TransactionType.AUTHORIZE, null, UUID.randomUUID().toString(), transactionExternalKey, authAmount, PaymentPluginStatus.PENDING);
+ Assert.assertEquals(initialPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
+
+ mockPaymentProviderPlugin.overridePaymentPluginStatus(initialPayment.getId(), initialPayment.getTransactions().get(0).getId(), PaymentPluginStatus.PROCESSED);
+
+ try {
+ final Payment completedPayment = createPayment(TransactionType.AUTHORIZE, initialPayment.getId(), initialPayment.getExternalKey(), transactionExternalKey, authAmount, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testVerifyJanitorFromUnknownDuringCompletionFlow() throws PaymentApiException {
+ final BigDecimal authAmount = BigDecimal.TEN;
+ final String transactionExternalKey = UUID.randomUUID().toString();
+
+ final Payment initialPayment = createPayment(TransactionType.AUTHORIZE, null, UUID.randomUUID().toString(), transactionExternalKey, authAmount, PaymentPluginStatus.UNDEFINED);
+ Assert.assertEquals(initialPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
+
+ mockPaymentProviderPlugin.overridePaymentPluginStatus(initialPayment.getId(), initialPayment.getTransactions().get(0).getId(), PaymentPluginStatus.PROCESSED);
+
+ try {
+ final Payment completedPayment = createPayment(TransactionType.AUTHORIZE, initialPayment.getId(), initialPayment.getExternalKey(), transactionExternalKey, authAmount, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
public void testNotifyPendingTransactionOfStateChanged() throws PaymentApiException {
final BigDecimal authAmount = BigDecimal.TEN;
@@ -1348,16 +1470,9 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
final String paymentExternalKey = "rouge";
final String transactionExternalKey = "vert";
- final Payment initialPayment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, authAmount, Currency.AED, paymentExternalKey, transactionExternalKey,
- ImmutableList.<PluginProperty>of(), callContext);
+ final Payment initialPayment = createPayment(TransactionType.AUTHORIZE, null, paymentExternalKey, transactionExternalKey, authAmount, PaymentPluginStatus.PENDING);
- // Update the payment/transaction by hand to simulate a PENDING state.
- final PaymentTransaction paymentTransaction = initialPayment.getTransactions().get(0);
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), initialPayment.getId(), TransactionType.AUTHORIZE, "AUTH_PENDING", "AUTH_PENDING",
- paymentTransaction.getId(), TransactionStatus.PENDING, paymentTransaction.getProcessedAmount(), paymentTransaction.getProcessedCurrency(),
- null, null, internalCallContext);
-
- final Payment payment = paymentApi.notifyPendingTransactionOfStateChanged(account, paymentTransaction.getId(), true, callContext);
+ final Payment payment = paymentApi.notifyPendingTransactionOfStateChanged(account, initialPayment.getTransactions().get(0).getId(), true, callContext);
assertEquals(payment.getExternalKey(), paymentExternalKey);
assertEquals(payment.getPaymentMethodId(), account.getPaymentMethodId());
@@ -1366,15 +1481,15 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
- assertEquals(payment.getCurrency(), Currency.AED);
+ assertEquals(payment.getCurrency(), Currency.USD);
assertEquals(payment.getTransactions().size(), 1);
assertEquals(payment.getTransactions().get(0).getExternalKey(), transactionExternalKey);
assertEquals(payment.getTransactions().get(0).getPaymentId(), payment.getId());
assertEquals(payment.getTransactions().get(0).getAmount().compareTo(authAmount), 0);
- assertEquals(payment.getTransactions().get(0).getCurrency(), Currency.AED);
+ assertEquals(payment.getTransactions().get(0).getCurrency(), Currency.USD);
assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(authAmount), 0);
- assertEquals(payment.getTransactions().get(0).getProcessedCurrency(), Currency.AED);
+ assertEquals(payment.getTransactions().get(0).getProcessedCurrency(), Currency.USD);
assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
assertEquals(payment.getTransactions().get(0).getTransactionType(), TransactionType.AUTHORIZE);
@@ -1429,7 +1544,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
ImmutableList.<PluginProperty>of(), callContext);
// Hack the Database to make it look like it was a failure
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, "AUTH_ERRORED", null,
+ paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), null, payment.getId(), TransactionType.AUTHORIZE, "AUTH_ERRORED", null,
payment.getTransactions().get(0).getId(), TransactionStatus.PLUGIN_FAILURE, null, null, null, null, internalCallContext);
final PaymentSqlDao paymentSqlDao = dbi.onDemand(PaymentSqlDao.class);
paymentSqlDao.updateLastSuccessPaymentStateName(payment.getId().toString(), "AUTH_ERRORED", null, internalCallContext);
@@ -1521,6 +1636,51 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
+ public void testCompletionOfUnknownAuthorization() throws Exception {
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ final Payment pendingPayment = createPayment(TransactionType.AUTHORIZE, null, paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.UNDEFINED);
+ assertNotNull(pendingPayment);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 1);
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
+
+ try {
+ // Attempt to complete the payment
+ createPayment(TransactionType.AUTHORIZE, pendingPayment.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testCompletionOfUnknownCapture() throws Exception {
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ final Payment authorization = createPayment(TransactionType.AUTHORIZE, null, paymentExternalKey, UUID.randomUUID().toString(), requestedAmount, PaymentPluginStatus.PROCESSED);
+ assertNotNull(authorization);
+ Assert.assertEquals(authorization.getTransactions().size(), 1);
+ Assert.assertEquals(authorization.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Payment pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.UNDEFINED);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 2);
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+
+ try {
+ // Attempt to complete the payment
+ createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
public void testCreatePurchaseWithTimeout() throws Exception {
final BigDecimal requestedAmount = BigDecimal.TEN;
final String paymentExternalKey = "ohhhh";
@@ -1555,7 +1715,6 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
@Test(groups = "slow")
public void testSanityAcrossTransactionTypes() throws PaymentApiException {
-
final BigDecimal requestedAmount = BigDecimal.TEN;
final String paymentExternalKey = "ahhhhhhhh";
final String transactionExternalKey = "okkkkkkk";
@@ -1570,7 +1729,6 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(pendingPayment.getTransactions().get(0).getExternalKey(), transactionExternalKey);
Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
-
try {
createPayment(TransactionType.PURCHASE, null, paymentExternalKey, transactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
Assert.fail("PURCHASE transaction with same key should have failed");
@@ -1581,10 +1739,8 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
@Test(groups = "slow")
public void testSuccessfulInitialTransactionToSameTransaction() throws Exception {
-
final BigDecimal requestedAmount = BigDecimal.TEN;
for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
-
final String paymentExternalKey = UUID.randomUUID().toString();
final String keyA = UUID.randomUUID().toString();
@@ -1600,6 +1756,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
createPayment(transactionType, processedPayment.getId(), paymentExternalKey, keyB, requestedAmount, PaymentPluginStatus.PROCESSED);
Assert.fail("Retrying initial successful transaction (AUTHORIZE, PURCHASE, CREDIT) with same different key should fail");
} catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
}
// Attempt to create another {AUTH, PURCHASE, CREDIT} with same key => key constraint should make the request fail
@@ -1607,17 +1764,15 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
createPayment(transactionType, processedPayment.getId(), paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.PROCESSED);
Assert.fail("Retrying initial successful transaction (AUTHORIZE, PURCHASE, CREDIT) with same transaction key should fail");
} catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
}
}
}
-
@Test(groups = "slow")
public void testPendingInitialTransactionToSameTransaction() throws Exception {
-
final BigDecimal requestedAmount = BigDecimal.TEN;
for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
-
final String paymentExternalKey = UUID.randomUUID().toString();
final String keyA = UUID.randomUUID().toString();
@@ -1633,6 +1788,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
createPayment(transactionType, pendingPayment.getId(), paymentExternalKey, keyB, requestedAmount, PaymentPluginStatus.PROCESSED);
Assert.fail("Retrying initial successful transaction (AUTHORIZE, PURCHASE, CREDIT) with same different key should fail");
} catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
}
// Attempt to create another {AUTH, PURCHASE, CREDIT} with same key => That should work because we are completing the payment
@@ -1643,13 +1799,10 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
}
}
-
@Test(groups = "slow")
public void testFailedInitialTransactionToSameTransactionWithSameKey() throws Exception {
-
final BigDecimal requestedAmount = BigDecimal.TEN;
for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
-
final String paymentExternalKey = UUID.randomUUID().toString();
final String keyA = UUID.randomUUID().toString();
@@ -1666,13 +1819,10 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
}
}
-
@Test(groups = "slow")
public void testFailedInitialTransactionToSameTransactionWithDifferentKey() throws Exception {
-
final BigDecimal requestedAmount = BigDecimal.TEN;
for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
-
final String paymentExternalKey = UUID.randomUUID().toString();
final String keyA = UUID.randomUUID().toString();
@@ -1691,7 +1841,292 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
}
}
+ @Test(groups = "slow")
+ public void testKeysSanityOnPending() throws Exception {
+ final String authKey = UUID.randomUUID().toString();
+ final Payment pendingAuthorization = createPayment(TransactionType.AUTHORIZE, null, null, authKey, BigDecimal.TEN, PaymentPluginStatus.PENDING);
+ assertNotNull(pendingAuthorization);
+ Assert.assertEquals(pendingAuthorization.getTransactions().size(), 1);
+ Assert.assertEquals(pendingAuthorization.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
+
+ try {
+ // Capture with the same transaction external key should fail
+ createPayment(TransactionType.CAPTURE, pendingAuthorization.getId(), null, authKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
+ }
+
+ final Account account1 = testHelper.createTestAccount("bobo2@gmail.com", true);
+ try {
+ // Different auth with the same payment external key on a different account should fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, pendingAuthorization.getExternalKey(), null, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ try {
+ // Different auth with the same payment external key but different transaction external key on a different account should fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, pendingAuthorization.getExternalKey(), UUID.randomUUID().toString(), BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ try {
+ // Different auth with the same transaction external key on a different account should fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, null, authKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ try {
+ // Auth with the same payment external key but different transaction external key should not go through
+ createPayment(TransactionType.AUTHORIZE, null, pendingAuthorization.getExternalKey(), UUID.randomUUID().toString(), BigDecimal.TEN, PaymentPluginStatus.PENDING);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ // Auth with the same payment and transaction external keys should go through (completion)
+ final Payment pendingAuthorization2 = createPayment(TransactionType.AUTHORIZE, null, pendingAuthorization.getExternalKey(), authKey, BigDecimal.TEN, PaymentPluginStatus.PENDING);
+ assertNotNull(pendingAuthorization2);
+ Assert.assertEquals(pendingAuthorization2.getTransactions().size(), 1);
+ Assert.assertEquals(pendingAuthorization2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
+
+ // Auth with the same transaction external key should go through (completion)
+ final Payment authorization = createPayment(TransactionType.AUTHORIZE, null, null, authKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ assertNotNull(authorization);
+ Assert.assertEquals(authorization.getTransactions().size(), 1);
+ Assert.assertEquals(authorization.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ try {
+ // Different auth with the same payment external key on a different account should still fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, pendingAuthorization.getExternalKey(), null, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ try {
+ // Different auth with the same payment external key but different transaction external key on a different account should still fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, pendingAuthorization.getExternalKey(), UUID.randomUUID().toString(), BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ try {
+ // Different auth with the same transaction external key on a different account should still fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, null, authKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ // Capture with a different transaction external key should go through
+ final String captureKey = UUID.randomUUID().toString();
+ final Payment pendingCapture = createPayment(TransactionType.CAPTURE, authorization.getId(), null, captureKey, BigDecimal.ONE, PaymentPluginStatus.PENDING);
+ Assert.assertEquals(pendingCapture.getTransactions().size(), 2);
+ Assert.assertEquals(pendingCapture.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingCapture.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
+
+ try {
+ // Different auth with the same transaction external key should fail
+ createPayment(TransactionType.AUTHORIZE, null, null, captureKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
+ }
+
+ try {
+ // Different auth with the same transaction external key on a different account should fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, null, captureKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ // Second capture with the same transaction external key should go through (completion)
+ final Payment capturedPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), null, captureKey, BigDecimal.ONE, PaymentPluginStatus.PROCESSED);
+ Assert.assertEquals(capturedPayment.getTransactions().size(), 2);
+ Assert.assertEquals(capturedPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(capturedPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ // Second capture with a different transaction external key should go through
+ final String captureKey2 = UUID.randomUUID().toString();
+ final Payment capturedPayment2 = createPayment(TransactionType.CAPTURE, authorization.getId(), null, captureKey2, BigDecimal.ONE, PaymentPluginStatus.PROCESSED);
+ Assert.assertEquals(capturedPayment2.getTransactions().size(), 3);
+ Assert.assertEquals(capturedPayment2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(capturedPayment2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(capturedPayment2.getTransactions().get(2).getTransactionStatus(), TransactionStatus.SUCCESS);
+ }
+
+ @Test(groups = "slow")
+ public void testKeysSanityOnSuccess() throws Exception {
+ final String authKey = UUID.randomUUID().toString();
+ final Payment authorization = createPayment(TransactionType.AUTHORIZE, null, null, authKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ assertNotNull(authorization);
+ Assert.assertEquals(authorization.getTransactions().size(), 1);
+ Assert.assertEquals(authorization.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ try {
+ // Capture with the same transaction external key should fail
+ createPayment(TransactionType.CAPTURE, authorization.getId(), null, authKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
+ }
+
+ try {
+ // Different auth with the same payment external key should fail
+ createPayment(TransactionType.AUTHORIZE, null, authorization.getExternalKey(), null, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+
+ try {
+ // Different auth with the same payment external key but different transaction external key should fail
+ createPayment(TransactionType.AUTHORIZE, null, authorization.getExternalKey(), UUID.randomUUID().toString(), BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+
+ try {
+ // Different auth with the same transaction external key should fail
+ createPayment(TransactionType.AUTHORIZE, null, null, authKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ final Account account1 = testHelper.createTestAccount("bobo2@gmail.com", true);
+ try {
+ // Different auth with the same payment external key on a different account should fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, authorization.getExternalKey(), UUID.randomUUID().toString(), BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ try {
+ // Different auth with the same payment external key but different transaction external key on a different account should fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, authorization.getExternalKey(), UUID.randomUUID().toString(), BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ try {
+ // Different auth with the same transaction external key on a different account should fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, null, authKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ // Capture with a different transaction external key should go through
+ final String captureKey = UUID.randomUUID().toString();
+ final Payment capturedPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), null, captureKey, BigDecimal.ONE, PaymentPluginStatus.PROCESSED);
+ Assert.assertEquals(capturedPayment.getTransactions().size(), 2);
+ Assert.assertEquals(capturedPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(capturedPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ try {
+ // Second capture with the same transaction external key should fail
+ createPayment(TransactionType.CAPTURE, authorization.getId(), null, captureKey, BigDecimal.ONE, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ try {
+ // Different auth with the same transaction external key should fail
+ createPayment(TransactionType.AUTHORIZE, null, null, captureKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
+ }
+
+ try {
+ // Different auth with the same transaction external key on a different account should fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, null, captureKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ // Second capture with a different transaction external key should go through
+ final String captureKey2 = UUID.randomUUID().toString();
+ final Payment capturedPayment2 = createPayment(TransactionType.CAPTURE, authorization.getId(), null, captureKey2, BigDecimal.ONE, PaymentPluginStatus.PROCESSED);
+ Assert.assertEquals(capturedPayment2.getTransactions().size(), 3);
+ Assert.assertEquals(capturedPayment2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(capturedPayment2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(capturedPayment2.getTransactions().get(2).getTransactionStatus(), TransactionStatus.SUCCESS);
+ }
+
+ @Test(groups = "slow")
+ public void testKeysSanityOnFailure() throws Exception {
+ final String authKey = UUID.randomUUID().toString();
+ final Payment failedAuthorization1 = createPayment(TransactionType.AUTHORIZE, null, null, authKey, BigDecimal.TEN, PaymentPluginStatus.ERROR);
+ assertNotNull(failedAuthorization1);
+ Assert.assertEquals(failedAuthorization1.getTransactions().size(), 1);
+ Assert.assertEquals(failedAuthorization1.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+
+ final Account account1 = testHelper.createTestAccount("bobo2@gmail.com", true);
+ try {
+ // Different auth with the same payment external key on a different account should fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, failedAuthorization1.getExternalKey(), null, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ try {
+ // Different auth with the same transaction external key on a different account should fail
+ createPayment(account1, TransactionType.AUTHORIZE, null, null, authKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ // Different auth with the same payment external key should go through
+ final Payment failedAuthorization2 = createPayment(TransactionType.AUTHORIZE, null, failedAuthorization1.getExternalKey(), null, BigDecimal.TEN, PaymentPluginStatus.ERROR);
+ assertNotNull(failedAuthorization2);
+ Assert.assertEquals(failedAuthorization2.getTransactions().size(), 2);
+ Assert.assertEquals(failedAuthorization2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertEquals(failedAuthorization2.getTransactions().get(0).getExternalKey(), authKey);
+ Assert.assertEquals(failedAuthorization2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertNotEquals(failedAuthorization2.getTransactions().get(1).getExternalKey(), authKey);
+
+ // Different auth with the same transaction external key should go through
+ final Payment failedAuthorization3 = createPayment(TransactionType.AUTHORIZE, null, null, authKey, BigDecimal.TEN, PaymentPluginStatus.ERROR);
+ assertNotNull(failedAuthorization3);
+ Assert.assertEquals(failedAuthorization3.getTransactions().size(), 3);
+ Assert.assertEquals(failedAuthorization3.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertEquals(failedAuthorization3.getTransactions().get(0).getExternalKey(), authKey);
+ Assert.assertEquals(failedAuthorization3.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertNotEquals(failedAuthorization3.getTransactions().get(1).getExternalKey(), authKey);
+ Assert.assertEquals(failedAuthorization3.getTransactions().get(2).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertEquals(failedAuthorization3.getTransactions().get(2).getExternalKey(), authKey);
+
+ // Different auth with the same payment external key but different transaction external key should go through
+ final Payment failedAuthorization4 = createPayment(TransactionType.AUTHORIZE, null, failedAuthorization1.getExternalKey(), UUID.randomUUID().toString(), BigDecimal.TEN, PaymentPluginStatus.ERROR);
+ assertNotNull(failedAuthorization4);
+ Assert.assertEquals(failedAuthorization4.getTransactions().size(), 4);
+ Assert.assertEquals(failedAuthorization4.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertEquals(failedAuthorization4.getTransactions().get(0).getExternalKey(), authKey);
+ Assert.assertEquals(failedAuthorization4.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertNotEquals(failedAuthorization4.getTransactions().get(1).getExternalKey(), authKey);
+ Assert.assertEquals(failedAuthorization4.getTransactions().get(2).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertEquals(failedAuthorization4.getTransactions().get(2).getExternalKey(), authKey);
+ Assert.assertEquals(failedAuthorization4.getTransactions().get(3).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertNotEquals(failedAuthorization4.getTransactions().get(3).getExternalKey(), authKey);
+ }
private void verifyRefund(final Payment refund, final String paymentExternalKey, final String paymentTransactionExternalKey, final String refundTransactionExternalKey, final BigDecimal requestedAmount, final BigDecimal refundAmount, final TransactionStatus transactionStatus) {
Assert.assertEquals(refund.getExternalKey(), paymentExternalKey);
@@ -1785,6 +2220,16 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
@Nullable final String paymentTransactionExternalKey,
@Nullable final BigDecimal amount,
final PaymentPluginStatus paymentPluginStatus) throws PaymentApiException {
+ return createPayment(account, transactionType, paymentId, paymentExternalKey, paymentTransactionExternalKey, amount, paymentPluginStatus);
+ }
+
+ private Payment createPayment(final Account account,
+ final TransactionType transactionType,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ @Nullable final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ final PaymentPluginStatus paymentPluginStatus) throws PaymentApiException {
final Iterable<PluginProperty> pluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, paymentPluginStatus.toString(), false));
switch (transactionType) {
case AUTHORIZE:
@@ -1817,6 +2262,14 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
paymentTransactionExternalKey,
pluginProperties,
callContext);
+ case CAPTURE:
+ return paymentApi.createCapture(account,
+ paymentId,
+ amount,
+ amount == null ? null : account.getCurrency(),
+ paymentTransactionExternalKey,
+ pluginProperties,
+ callContext);
default:
Assert.fail();
return null;
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java
index 83a2b65..cad7b12 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java
@@ -88,7 +88,7 @@ public class TestPaymentApiNoDB extends PaymentTestSuiteNoDB {
@Test(groups = "fast")
public void testSimpleInvoicePaymentWithInvoiceAmount() throws Exception {
- final BigDecimal invoiceAmount = new BigDecimal("10.0011");
+ final BigDecimal invoiceAmount = BigDecimal.TEN;
final BigDecimal requestedAmount = invoiceAmount;
final BigDecimal expectedAmount = invoiceAmount;
@@ -97,8 +97,8 @@ public class TestPaymentApiNoDB extends PaymentTestSuiteNoDB {
@Test(groups = "fast")
public void testSimpleInvoicePaymentWithLowerAmount() throws Exception {
- final BigDecimal invoiceAmount = new BigDecimal("10.0011");
- final BigDecimal requestedAmount = new BigDecimal("8.0091");
+ final BigDecimal invoiceAmount = BigDecimal.TEN;
+ final BigDecimal requestedAmount = BigDecimal.ONE;
final BigDecimal expectedAmount = requestedAmount;
testSimplePayment(invoiceAmount, requestedAmount, expectedAmount);
@@ -106,8 +106,8 @@ public class TestPaymentApiNoDB extends PaymentTestSuiteNoDB {
@Test(groups = "fast")
public void testSimpleInvoicePaymentWithInvalidAmount() throws Exception {
- final BigDecimal invoiceAmount = new BigDecimal("10.0011");
- final BigDecimal requestedAmount = new BigDecimal("80.0091");
+ final BigDecimal invoiceAmount = BigDecimal.ONE;
+ final BigDecimal requestedAmount = BigDecimal.TEN;
final BigDecimal expectedAmount = null;
testSimplePayment(invoiceAmount, requestedAmount, expectedAmount);
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
index acb1be0..15d8dac 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
@@ -21,6 +21,7 @@ import java.math.BigDecimal;
import java.util.List;
import java.util.UUID;
+import org.killbill.billing.ErrorCode;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.control.plugin.api.OnFailurePaymentControlResult;
@@ -32,11 +33,15 @@ import org.killbill.billing.control.plugin.api.PriorPaymentControlResult;
import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
import org.killbill.billing.osgi.api.OSGIServiceRegistration;
import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
import org.killbill.billing.payment.provider.DefaultNoOpPaymentMethodPlugin;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
import org.killbill.billing.payment.retry.DefaultFailureCallResult;
import org.killbill.billing.payment.retry.DefaultOnSuccessPaymentControlResult;
+import org.killbill.commons.request.Request;
+import org.killbill.commons.request.RequestData;
import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -86,6 +91,14 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
}
},
testPaymentControlPluginApi);
+
+ // Required for re-entrant locks to work
+ Request.setPerThreadRequestData(new RequestData(UUID.randomUUID().toString()));
+ }
+
+ @AfterMethod(groups = "slow")
+ public void tearDown() throws Exception {
+ Request.resetPerThreadRequestData();
}
// Verify Payment control API can be used to change the paymentMethodId on the fly and this is reflected in the created Payment.
@@ -101,6 +114,378 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
+ public void testCreateAuthPendingWithControlCompleteWithControl() throws PaymentApiException {
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
+ Payment payment = paymentApi.createAuthorizationWithPaymentControl(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ paymentTransactionExternalKey, pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ payment = paymentApi.createAuthorizationWithPaymentControl(account, payment.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
+ payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthUnknownWithControlCompleteWithControl() throws PaymentApiException {
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ Payment payment = paymentApi.createAuthorizationWithPaymentControl(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ paymentTransactionExternalKey, pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ try {
+ payment = paymentApi.createAuthorizationWithPaymentControl(account, payment.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
+ payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthSuccessCapturePendingWithControlCompleteWithControl() throws PaymentApiException {
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthSuccessCaptureUnknownWithControlCompleteWithControl() throws PaymentApiException {
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ try {
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthPendingWithControlCompleteNoControl() throws PaymentApiException {
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
+ Payment payment = paymentApi.createAuthorizationWithPaymentControl(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ paymentTransactionExternalKey, pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
+ payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthUnknownWithControlCompleteNoControl() throws PaymentApiException {
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ Payment payment = paymentApi.createAuthorizationWithPaymentControl(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ paymentTransactionExternalKey, pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ try {
+ payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
+ payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthSuccessCapturePendingWithControlCompleteNoControl() throws PaymentApiException {
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ payment = paymentApi.createCapture(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey, ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthSuccessCaptureUnknownWithControlCompleteNoControl() throws PaymentApiException {
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ try {
+ payment = paymentApi.createCapture(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey, ImmutableList.<PluginProperty>of(), callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthPendingNoControlCompleteWithControl() throws PaymentApiException {
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ paymentTransactionExternalKey, pendingPluginProperties, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ payment = paymentApi.createAuthorizationWithPaymentControl(account, payment.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
+ payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthUnknownNoControlCompleteWithControl() throws PaymentApiException {
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ paymentTransactionExternalKey, pendingPluginProperties, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ try {
+ payment = paymentApi.createAuthorizationWithPaymentControl(account, payment.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
+ payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+
+ @Test(groups = "slow")
+ public void testCreateAuthSuccessCapturePendingNoControlCompleteWithControl() throws PaymentApiException {
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
+ payment = paymentApi.createCapture(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey, pendingPluginProperties, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthSuccessCaptureUnknownNoControlCompleteWithControl() throws PaymentApiException {
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ payment = paymentApi.createCapture(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey, pendingPluginProperties, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ try {
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
public void testCreateAuthWithControlCaptureNoControl() throws PaymentApiException {
final BigDecimal requestedAmount = BigDecimal.TEN;
@@ -174,7 +559,7 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
@Override
public Iterable<PluginProperty> getAdjustedPluginProperties() {
- return ImmutableList.of();
+ return null;
}
};
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCache.java b/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCache.java
new file mode 100644
index 0000000..9960e7a
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCache.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 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.payment.caching;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.killbill.automaton.StateMachineConfig;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.payment.PaymentTestSuiteNoDB;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.glue.PaymentModule;
+import org.killbill.xmlloader.UriAccessor;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Resources;
+import net.sf.ehcache.CacheException;
+
+public class TestStateMachineConfigCache extends PaymentTestSuiteNoDB {
+
+ private InternalTenantContext multiTenantContext;
+ private InternalTenantContext otherMultiTenantContext;
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+
+ cacheControllerDispatcher.clearAll();
+
+ multiTenantContext = Mockito.mock(InternalTenantContext.class);
+ Mockito.when(multiTenantContext.getAccountRecordId()).thenReturn(456L);
+ Mockito.when(multiTenantContext.getTenantRecordId()).thenReturn(99L);
+
+ otherMultiTenantContext = Mockito.mock(InternalCallContext.class);
+ Mockito.when(otherMultiTenantContext.getAccountRecordId()).thenReturn(123L);
+ Mockito.when(otherMultiTenantContext.getTenantRecordId()).thenReturn(112233L);
+ }
+
+ @Test(groups = "fast")
+ public void testMissingPluginStateMachineConfig() throws PaymentApiException {
+ Assert.assertNotNull(stateMachineConfigCache.getPaymentStateMachineConfig(UUID.randomUUID().toString(), internalCallContext));
+ Assert.assertNotNull(stateMachineConfigCache.getPaymentStateMachineConfig(UUID.randomUUID().toString(), multiTenantContext));
+ Assert.assertNotNull(stateMachineConfigCache.getPaymentStateMachineConfig(UUID.randomUUID().toString(), otherMultiTenantContext));
+ }
+
+ @Test(groups = "fast")
+ public void testExistingTenantStateMachineConfig() throws PaymentApiException, URISyntaxException, IOException {
+ final String pluginName = UUID.randomUUID().toString();
+
+ final InternalCallContext differentMultiTenantContext = Mockito.mock(InternalCallContext.class);
+ Mockito.when(differentMultiTenantContext.getTenantRecordId()).thenReturn(55667788L);
+
+ final AtomicBoolean shouldThrow = new AtomicBoolean(false);
+ final Long multiTenantRecordId = multiTenantContext.getTenantRecordId();
+ final Long otherMultiTenantRecordId = otherMultiTenantContext.getTenantRecordId();
+
+ Mockito.when(tenantInternalApi.getPluginPaymentStateMachineConfig(Mockito.eq(pluginName), Mockito.any(InternalTenantContext.class))).thenAnswer(new Answer<String>() {
+ @Override
+ public String answer(final InvocationOnMock invocation) throws Throwable {
+ if (shouldThrow.get()) {
+ throw new RuntimeException();
+ }
+ final InternalTenantContext internalContext = (InternalTenantContext) invocation.getArguments()[1];
+ if (multiTenantRecordId.equals(internalContext.getTenantRecordId())) {
+ return new String(ByteStreams.toByteArray(UriAccessor.accessUri(Resources.getResource(PaymentModule.DEFAULT_STATE_MACHINE_PAYMENT_XML).toExternalForm())));
+ } else if (otherMultiTenantRecordId.equals(internalContext.getTenantRecordId())) {
+ return new String(ByteStreams.toByteArray(UriAccessor.accessUri(Resources.getResource(PaymentModule.DEFAULT_STATE_MACHINE_RETRY_XML).toExternalForm())));
+ } else {
+ return null;
+ }
+ }
+ });
+
+ // Verify the lookup for a non-cached tenant. No system config is set yet but EhCacheStateMachineConfigCache returns a default empty one
+ final StateMachineConfig defaultStateMachineConfig = stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, differentMultiTenantContext);
+ Assert.assertNotNull(defaultStateMachineConfig);
+
+ // Make sure the cache loader isn't invoked, see https://github.com/killbill/killbill/issues/300
+ shouldThrow.set(true);
+
+ final StateMachineConfig defaultStateMachineConfig2 = stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, differentMultiTenantContext);
+ Assert.assertNotNull(defaultStateMachineConfig2);
+ Assert.assertEquals(defaultStateMachineConfig2, defaultStateMachineConfig);
+
+ shouldThrow.set(false);
+
+ // Verify the lookup for this tenant
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(UUID.randomUUID().toString(), multiTenantContext), defaultStateMachineConfig);
+ final StateMachineConfig result = stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, multiTenantContext);
+ Assert.assertNotNull(result);
+ Assert.assertNotEquals(result, defaultStateMachineConfig);
+ Assert.assertEquals(result.getStateMachines().length, 8);
+
+ // Verify the lookup for another tenant
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(UUID.randomUUID().toString(), otherMultiTenantContext), defaultStateMachineConfig);
+ final StateMachineConfig otherResult = stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, otherMultiTenantContext);
+ Assert.assertNotNull(otherResult);
+ Assert.assertEquals(otherResult.getStateMachines().length, 1);
+
+ shouldThrow.set(true);
+
+ // Verify the lookup for this tenant
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, multiTenantContext), result);
+
+ // Verify the lookup with another context for the same tenant
+ final InternalCallContext sameMultiTenantContext = Mockito.mock(InternalCallContext.class);
+ Mockito.when(sameMultiTenantContext.getAccountRecordId()).thenReturn(9102L);
+ Mockito.when(sameMultiTenantContext.getTenantRecordId()).thenReturn(multiTenantRecordId);
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, sameMultiTenantContext), result);
+
+ // Verify the lookup with the other tenant
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, otherMultiTenantContext), otherResult);
+
+ // Verify clearing the cache works
+ stateMachineConfigCache.clearPaymentStateMachineConfig(pluginName, multiTenantContext);
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, otherMultiTenantContext), otherResult);
+ try {
+ stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, multiTenantContext);
+ Assert.fail();
+ } catch (final CacheException exception) {
+ Assert.assertTrue(exception.getCause() instanceof RuntimeException);
+ }
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCacheInvalidationCallback.java b/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCacheInvalidationCallback.java
new file mode 100644
index 0000000..9ead7cd
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCacheInvalidationCallback.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 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.payment.caching;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.killbill.automaton.StateMachineConfig;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.payment.PaymentTestSuiteNoDB;
+import org.killbill.billing.tenant.api.TenantKV.TenantKey;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.sf.ehcache.CacheException;
+
+public class TestStateMachineConfigCacheInvalidationCallback extends PaymentTestSuiteNoDB {
+
+ private InternalTenantContext multiTenantContext;
+ private InternalTenantContext otherMultiTenantContext;
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+
+ cacheControllerDispatcher.clearAll();
+
+ multiTenantContext = Mockito.mock(InternalTenantContext.class);
+ Mockito.when(multiTenantContext.getAccountRecordId()).thenReturn(456L);
+ Mockito.when(multiTenantContext.getTenantRecordId()).thenReturn(99L);
+
+ otherMultiTenantContext = Mockito.mock(InternalCallContext.class);
+ Mockito.when(otherMultiTenantContext.getAccountRecordId()).thenReturn(123L);
+ Mockito.when(otherMultiTenantContext.getTenantRecordId()).thenReturn(112233L);
+ }
+
+ @Test(groups = "fast")
+ public void testInvalidation() throws Exception {
+ final String pluginName = UUID.randomUUID().toString();
+
+ final StateMachineConfig defaultPaymentStateMachineConfig = stateMachineConfigCache.getPaymentStateMachineConfig(UUID.randomUUID().toString(), internalCallContext);
+ Assert.assertNotNull(defaultPaymentStateMachineConfig);
+
+ final AtomicBoolean shouldThrow = new AtomicBoolean(false);
+
+ Mockito.when(tenantInternalApi.getPluginPaymentStateMachineConfig(Mockito.eq(pluginName), Mockito.any(InternalTenantContext.class))).thenAnswer(new Answer<String>() {
+ @Override
+ public String answer(final InvocationOnMock invocation) throws Throwable {
+ if (shouldThrow.get()) {
+ throw new RuntimeException();
+ }
+ return null;
+ }
+ });
+
+ // Prime caches
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, internalCallContext), defaultPaymentStateMachineConfig);
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, otherMultiTenantContext), defaultPaymentStateMachineConfig);
+
+ shouldThrow.set(true);
+
+ // No exception (cached)
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, internalCallContext), defaultPaymentStateMachineConfig);
+
+ cacheInvalidationCallback.invalidateCache(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_, pluginName, multiTenantContext);
+
+ try {
+ stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, multiTenantContext);
+ Assert.fail();
+ } catch (final CacheException exception) {
+ Assert.assertTrue(exception.getCause() instanceof RuntimeException);
+ }
+
+ // No exception (cached)
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, otherMultiTenantContext), defaultPaymentStateMachineConfig);
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentAutomatonDAOHelper.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentAutomatonDAOHelper.java
index f6dcd0a..65586e8 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentAutomatonDAOHelper.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentAutomatonDAOHelper.java
@@ -100,7 +100,7 @@ public class TestPaymentAutomatonDAOHelper extends PaymentTestSuiteWithEmbeddedD
public void testNoPaymentMethod() throws Exception {
final PaymentAutomatonDAOHelper daoHelper = createDAOHelper(UUID.randomUUID(), paymentExternalKey, paymentTransactionExternalKey, amount, currency);
try {
- daoHelper.getPaymentProviderPlugin();
+ daoHelper.getPaymentPluginApi();
Assert.fail();
} catch (final PaymentApiException e) {
Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD.getCode());
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java
index 1ef93cc..38a1e9b 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java
@@ -88,59 +88,6 @@ public class TestPaymentLeavingStateCallback extends PaymentTestSuiteWithEmbedde
Assert.assertEquals(paymentDao.getTransactionsForPayment(paymentId, internalCallContext).size(), 2);
}
- @Test(groups = "slow", expectedExceptions = OperationException.class)
- public void testLeaveStateForConflictingPaymentTransactionExternalKey() throws Exception {
- final UUID paymentId = UUID.randomUUID();
- setUp(paymentId);
-
- // Verify the payment has only one transaction
- final List<PaymentTransactionModelDao> transactions = paymentDao.getTransactionsForPayment(paymentId, internalCallContext);
- Assert.assertEquals(transactions.size(), 1);
-
- final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.CAPTURE).toString();
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), paymentId, TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
- transactions.get(0).getId(), TransactionStatus.SUCCESS, BigDecimal.ONE, Currency.BRL,
- "foo", "bar", internalCallContext);
-
- // Will validate the validateUniqueTransactionExternalKey logic for when we reuse the same payment transactionExternalKey
- callback.leavingState(state);
-
- }
-
- @Test(groups = "slow", expectedExceptions = OperationException.class)
- public void testLeaveStateForConflictingPaymentTransactionExternalKeyAcrossAccounts() throws Exception {
- final UUID paymentId = UUID.randomUUID();
- setUp(paymentId);
-
- // Verify the payment has only one transaction
- final List<PaymentTransactionModelDao> transactions = paymentDao.getTransactionsForPayment(paymentId, internalCallContext);
- Assert.assertEquals(transactions.size(), 1);
-
- final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.CAPTURE).toString();
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), paymentId, TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
- transactions.get(0).getId(), TransactionStatus.SUCCESS, BigDecimal.ONE, Currency.BRL,
- "foo", "bar", internalCallContext);
-
- paymentStateContext = new PaymentStateContext(true,
- paymentId,
- null,
- null,
- paymentStateContext.getPaymentExternalKey(),
- paymentStateContext.getPaymentTransactionExternalKey(),
- paymentStateContext.getTransactionType(),
- paymentStateContext.getAccount(),
- paymentStateContext.getPaymentMethodId(),
- paymentStateContext.getAmount(),
- paymentStateContext.getCurrency(),
- paymentStateContext.shouldLockAccountAndDispatch(),
- paymentStateContext.getOverridePluginOperationResult(),
- paymentStateContext.getProperties(),
- internalCallContext,
- callContext);
-
- callback.leavingState(state);
- }
-
private void verifyPaymentTransaction() {
Assert.assertNotNull(paymentStateContext.getPaymentTransactionModelDao().getPaymentId());
Assert.assertEquals(paymentStateContext.getPaymentTransactionModelDao().getTransactionExternalKey(), paymentStateContext.getPaymentTransactionExternalKey());
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java
index 2e642c9..e34c632 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java
@@ -209,7 +209,7 @@ public class TestPluginOperation extends PaymentTestSuiteNoDB {
callContext);
final PaymentAutomatonDAOHelper daoHelper = Mockito.mock(PaymentAutomatonDAOHelper.class);
- Mockito.when(daoHelper.getPaymentProviderPlugin()).thenReturn(null);
+ Mockito.when(daoHelper.getPaymentPluginApi()).thenReturn(null);
return new PluginOperationTest(daoHelper, locker, paymentPluginDispatcher, paymentConfig, paymentStateContext);
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
index 5f87219..4f3b429 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
@@ -80,9 +80,6 @@ import static org.testng.Assert.fail;
public class TestRetryablePayment extends PaymentTestSuiteNoDB {
@Inject
- @Named(PaymentModule.STATE_MACHINE_PAYMENT)
- private StateMachineConfig stateMachineConfig;
- @Inject
@Named(PaymentModule.STATE_MACHINE_RETRY)
private StateMachineConfig retryStateMachineConfig;
@Inject
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentProcessor.java b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentProcessor.java
index d60f90b..fe0fe2a 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentProcessor.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentProcessor.java
@@ -18,7 +18,6 @@
package org.killbill.billing.payment.core;
import java.math.BigDecimal;
-import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@@ -28,9 +27,9 @@ import javax.annotation.Nullable;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.catalog.api.Currency;
-import org.killbill.billing.events.BusInternalEvent;
import org.killbill.billing.events.PaymentErrorInternalEvent;
import org.killbill.billing.events.PaymentInfoInternalEvent;
+import org.killbill.billing.events.PaymentInternalEvent;
import org.killbill.billing.events.PaymentPluginErrorInternalEvent;
import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
import org.killbill.billing.payment.api.Payment;
@@ -58,11 +57,14 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
private static final BigDecimal TEN = new BigDecimal("10");
private static final Currency CURRENCY = Currency.BTC;
+ private MockPaymentProviderPlugin mockPaymentProviderPlugin;
private PaymentBusListener paymentBusListener;
private Account account;
@BeforeMethod(groups = "slow")
public void setUp() throws Exception {
+ mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(MockPaymentProviderPlugin.PLUGIN_NAME);
+
account = testHelper.createTestAccount(UUID.randomUUID().toString(), true);
paymentBusListener = new PaymentBusListener();
@@ -70,9 +72,31 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
- public void testClassicFlow() throws Exception {
+ public void testGetAccountPaymentsWithJanitor() throws Exception {
final String paymentExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pluginPropertiesToDriveTransationToUnknown = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ final String authorizationKey = UUID.randomUUID().toString();
+ final Payment authorization = paymentProcessor.createAuthorization(true, null, account, null, null, TEN, CURRENCY, paymentExternalKey, authorizationKey,
+ SHOULD_LOCK_ACCOUNT, pluginPropertiesToDriveTransationToUnknown, callContext, internalCallContext);
+ verifyPayment(authorization, paymentExternalKey, ZERO, ZERO, ZERO, 1);
+ final UUID paymentId = authorization.getId();
+ verifyPaymentTransaction(authorization.getTransactions().get(0), authorizationKey, TransactionType.AUTHORIZE, TEN, paymentId);
+ paymentBusListener.verify(0, 0, 1, account.getId(), paymentId, ZERO, TransactionStatus.UNKNOWN);
+
+ mockPaymentProviderPlugin.overridePaymentPluginStatus(paymentId, authorization.getTransactions().get(0).getId(), PaymentPluginStatus.PROCESSED);
+
+ final List<Payment> payments = paymentProcessor.getAccountPayments(account.getId(), true, callContext, internalCallContext);
+ Assert.assertEquals(payments.size(), 1);
+ verifyPayment(payments.get(0), paymentExternalKey, TEN, ZERO, ZERO, 1);
+ verifyPaymentTransaction(payments.get(0).getTransactions().get(0), authorizationKey, TransactionType.AUTHORIZE, TEN, paymentId);
+ paymentBusListener.verify(1, 0, 1, account.getId(), paymentId, TEN, TransactionStatus.SUCCESS);
+ }
+
+ @Test(groups = "slow")
+ public void testClassicFlow() throws Exception {
+ final String paymentExternalKey = UUID.randomUUID().toString();
final Iterable<PluginProperty> pluginPropertiesToDriveTransationToPending = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
@@ -209,8 +233,8 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
private static final class PaymentBusListener {
private final List<PaymentInfoInternalEvent> paymentInfoEvents = new LinkedList<PaymentInfoInternalEvent>();
- private final Collection<BusInternalEvent> paymentErrorEvents = new LinkedList<BusInternalEvent>();
- private final Collection<BusInternalEvent> paymentPluginErrorEvents = new LinkedList<BusInternalEvent>();
+ private final List<PaymentInternalEvent> paymentErrorEvents = new LinkedList<PaymentInternalEvent>();
+ private final List<PaymentInternalEvent> paymentPluginErrorEvents = new LinkedList<PaymentInternalEvent>();
@Subscribe
public void paymentInfo(final PaymentInfoInternalEvent event) {
@@ -228,20 +252,28 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
}
private void verify(final int eventNb, final UUID accountId, final UUID paymentId, final BigDecimal amount, final TransactionStatus transactionStatus) throws Exception {
+ verify(eventNb, 0, 0, accountId, paymentId, amount, transactionStatus);
+ }
+
+ private void verify(final int nbInfoEvents, final int nbErrorEvents, final int nbPluginErrorEvents, final UUID accountId, final UUID paymentId, final BigDecimal amount, final TransactionStatus transactionStatus) throws Exception {
Awaitility.await()
.until(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
- return paymentInfoEvents.size() == eventNb;
+ return paymentInfoEvents.size() == nbInfoEvents && paymentErrorEvents.size() == nbErrorEvents && paymentPluginErrorEvents.size() == nbPluginErrorEvents;
}
});
- Assert.assertEquals(paymentErrorEvents.size(), 0);
- Assert.assertEquals(paymentPluginErrorEvents.size(), 0);
- verify(paymentInfoEvents.get(eventNb - 1), accountId, paymentId, amount, transactionStatus);
+ if (transactionStatus == TransactionStatus.SUCCESS || transactionStatus == TransactionStatus.PENDING) {
+ verify(paymentInfoEvents.get(paymentInfoEvents.size() - 1), accountId, paymentId, amount, transactionStatus);
+ } else if (transactionStatus == TransactionStatus.PAYMENT_FAILURE) {
+ verify(paymentErrorEvents.get(paymentErrorEvents.size() - 1), accountId, paymentId, amount, transactionStatus);
+ } else {
+ verify(paymentPluginErrorEvents.get(paymentPluginErrorEvents.size() - 1), accountId, paymentId, amount, transactionStatus);
+ }
}
- private void verify(final PaymentInfoInternalEvent event, final UUID accountId, final UUID paymentId, @Nullable final BigDecimal amount, final TransactionStatus transactionStatus) {
+ private void verify(final PaymentInternalEvent event, final UUID accountId, final UUID paymentId, @Nullable final BigDecimal amount, final TransactionStatus transactionStatus) {
Assert.assertEquals(event.getPaymentId(), paymentId);
Assert.assertEquals(event.getAccountId(), accountId);
if (amount == null) {
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
index 3f314c8..0798535 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
@@ -221,7 +221,7 @@ public class MockPaymentDao implements PaymentDao {
}
@Override
- public void updatePaymentAndTransactionOnCompletion(final UUID accountId, final UUID paymentId, final TransactionType transactionType,
+ public void updatePaymentAndTransactionOnCompletion(final UUID accountId, final UUID attemptId, final UUID paymentId, final TransactionType transactionType,
final String currentPaymentStateName, final String lastSuccessPaymentStateName, final UUID transactionId,
final TransactionStatus paymentStatus, final BigDecimal processedAmount, final Currency processedCurrency,
final String gatewayErrorCode, final String gatewayErrorMsg, final InternalCallContext context) {
@@ -232,6 +232,7 @@ public class MockPaymentDao implements PaymentDao {
}
final PaymentTransactionModelDao transaction = transactions.get(transactionId);
if (transaction != null) {
+ transaction.setAttemptId(attemptId);
transaction.setTransactionStatus(paymentStatus);
transaction.setProcessedAmount(processedAmount);
transaction.setProcessedCurrency(processedCurrency);
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java
index 5b56ec4..4d6a938 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java
@@ -65,6 +65,7 @@ public class TestDefaultPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
final String gatewayErrorCode = UUID.randomUUID().toString().substring(0, 5);
final String gatewayErrorMsg = UUID.randomUUID().toString();
paymentDao.updatePaymentAndTransactionOnCompletion(accountId,
+ specifiedSecondPaymentTransactionModelDao.getAttemptId(),
specifiedSecondPaymentTransactionModelDao.getPaymentId(),
specifiedFirstPaymentTransactionModelDao.getTransactionType(),
"SOME_ERRORED_STATE",
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 91a98e1..a195593 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
@@ -165,7 +165,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
final List<PaymentTransactionModelDao> transactions = paymentDao.getTransactionsForPayment(savedPayment.getId(), internalCallContext);
assertEquals(transactions.size(), 2);
- paymentDao.updatePaymentAndTransactionOnCompletion(accountId, savedPayment.getId(), savedTransactionModelDao2.getTransactionType(), "AUTH_ABORTED", "AUTH_SUCCESS", transactionModelDao2.getId(), TransactionStatus.SUCCESS,
+ paymentDao.updatePaymentAndTransactionOnCompletion(accountId, savedTransactionModelDao2.getAttemptId(), savedPayment.getId(), savedTransactionModelDao2.getTransactionType(), "AUTH_ABORTED", "AUTH_SUCCESS", transactionModelDao2.getId(), TransactionStatus.SUCCESS,
BigDecimal.ONE, Currency.USD, null, "nothing", internalCallContext);
final PaymentModelDao savedPayment4 = paymentDao.getPayment(savedPayment.getId(), internalCallContext);
@@ -188,7 +188,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
assertNull(savedTransactionModelDao4.getGatewayErrorCode());
assertEquals(savedTransactionModelDao4.getGatewayErrorMsg(), "nothing");
- paymentDao.updatePaymentAndTransactionOnCompletion(accountId, savedPayment.getId(), savedTransactionModelDao2.getTransactionType(), "AUTH_ABORTED", null, transactionModelDao2.getId(), TransactionStatus.SUCCESS,
+ paymentDao.updatePaymentAndTransactionOnCompletion(accountId, savedTransactionModelDao2.getAttemptId(), savedPayment.getId(), savedTransactionModelDao2.getTransactionType(), "AUTH_ABORTED", null, transactionModelDao2.getId(), TransactionStatus.SUCCESS,
BigDecimal.ONE, Currency.USD, null, "nothing", internalCallContext);
final PaymentModelDao savedPayment4Again = paymentDao.getPayment(savedPayment.getId(), internalCallContext);
@@ -196,7 +196,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(savedPayment4Again.getStateName(), "AUTH_ABORTED");
assertEquals(savedPayment4Again.getLastSuccessStateName(), "AUTH_SUCCESS");
- paymentDao.updatePaymentAndTransactionOnCompletion(accountId, savedPayment.getId(), savedTransactionModelDao2.getTransactionType(), "AUTH_ABORTED", "AUTH_SUCCESS", transactionModelDao2.getId(), TransactionStatus.SUCCESS,
+ paymentDao.updatePaymentAndTransactionOnCompletion(accountId, savedTransactionModelDao2.getAttemptId(), savedPayment.getId(), savedTransactionModelDao2.getTransactionType(), "AUTH_ABORTED", "AUTH_SUCCESS", transactionModelDao2.getId(), TransactionStatus.SUCCESS,
BigDecimal.ONE, Currency.USD, null, "nothing", internalCallContext);
final PaymentModelDao savedPayment4Final = paymentDao.getPayment(savedPayment.getId(), internalCallContext);
@@ -300,7 +300,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
final Iterable<PaymentTransactionModelDao> transactions1 = paymentDao.getByTransactionStatusAcrossTenants(ImmutableList.of(TransactionStatus.PENDING), newTime, initialTime, 0L, 3L);
for (PaymentTransactionModelDao paymentTransaction : transactions1) {
final String newPaymentState = "XXX_FAILED";
- paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), payment.getId(), paymentTransaction.getTransactionType(), newPaymentState, payment.getLastSuccessStateName(),
+ paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), paymentTransaction.getAttemptId(), payment.getId(), paymentTransaction.getTransactionType(), newPaymentState, payment.getLastSuccessStateName(),
paymentTransaction.getId(), TransactionStatus.PAYMENT_FAILURE, paymentTransaction.getProcessedAmount(), paymentTransaction.getProcessedCurrency(),
paymentTransaction.getGatewayErrorCode(), paymentTransaction.getGatewayErrorMsg(), internalCallContext);
}
@@ -317,7 +317,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
final Iterable<PaymentTransactionModelDao> transactions2 = paymentDao.getByTransactionStatusAcrossTenants(ImmutableList.of(TransactionStatus.PENDING), clock.getUTCNow(), initialTime, 0L, 1L);
for (PaymentTransactionModelDao paymentTransaction : transactions2) {
final String newPaymentState = "XXX_FAILED";
- paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), payment.getId(), paymentTransaction.getTransactionType(), newPaymentState, payment.getLastSuccessStateName(),
+ paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), paymentTransaction.getAttemptId(), payment.getId(), paymentTransaction.getTransactionType(), newPaymentState, payment.getLastSuccessStateName(),
paymentTransaction.getId(), TransactionStatus.PAYMENT_FAILURE, paymentTransaction.getProcessedAmount(), paymentTransaction.getProcessedCurrency(),
paymentTransaction.getGatewayErrorCode(), paymentTransaction.getGatewayErrorMsg(), internalCallContext);
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
index 1b8eecb..b50ce2c 100644
--- a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
@@ -18,12 +18,15 @@
package org.killbill.billing.payment;
+import javax.inject.Named;
+
import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.invoice.api.InvoiceInternalApi;
import org.killbill.billing.osgi.api.OSGIServiceRegistration;
import org.killbill.billing.payment.api.PaymentApi;
import org.killbill.billing.payment.api.PaymentGatewayApi;
+import org.killbill.billing.payment.caching.StateMachineConfigCache;
import org.killbill.billing.payment.core.PaymentExecutors;
import org.killbill.billing.payment.core.PaymentMethodProcessor;
import org.killbill.billing.payment.core.PaymentProcessor;
@@ -32,11 +35,14 @@ import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner;
import org.killbill.billing.payment.dao.MockPaymentDao;
import org.killbill.billing.payment.dao.PaymentDao;
+import org.killbill.billing.payment.glue.PaymentModule;
import org.killbill.billing.payment.glue.TestPaymentModuleNoDB;
import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
import org.killbill.billing.payment.retry.DefaultRetryService;
import org.killbill.billing.platform.api.KillbillConfigSource;
+import org.killbill.billing.tenant.api.TenantInternalApi;
+import org.killbill.billing.tenant.api.TenantInternalApi.CacheInvalidationCallback;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.config.definition.PaymentConfig;
import org.killbill.bus.api.PersistentBus;
@@ -50,6 +56,8 @@ import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
+import static org.killbill.billing.payment.provider.MockPaymentControlProviderPlugin.PLUGIN_NAME;
+
public abstract class PaymentTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
@Inject
@@ -86,6 +94,13 @@ public abstract class PaymentTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
protected CacheControllerDispatcher cacheControllerDispatcher;
@Inject
protected PaymentExecutors paymentExecutors;
+ @Inject
+ protected StateMachineConfigCache stateMachineConfigCache;
+ @Inject
+ @Named(PaymentModule.STATE_MACHINE_CONFIG_INVALIDATION_CALLBACK)
+ protected CacheInvalidationCallback cacheInvalidationCallback;
+ @Inject
+ protected TenantInternalApi tenantInternalApi;
@Override
protected KillbillConfigSource getConfigSource() {
@@ -103,6 +118,9 @@ public abstract class PaymentTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
@BeforeMethod(groups = "fast")
public void beforeMethod() throws Exception {
+ stateMachineConfigCache.clearPaymentStateMachineConfig(PLUGIN_NAME, internalCallContext);
+ stateMachineConfigCache.loadDefaultPaymentStateMachineConfig(PaymentModule.DEFAULT_STATE_MACHINE_PAYMENT_XML);
+
eventBus.start();
paymentExecutors.initialize();
((MockPaymentDao) paymentDao).reset();
diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
index 655b60a..73cc4ea 100644
--- a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * 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
@@ -23,13 +23,16 @@ import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.control.plugin.api.PaymentControlPluginApi;
import org.killbill.billing.invoice.api.InvoiceInternalApi;
import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.api.AdminPaymentApi;
import org.killbill.billing.payment.api.PaymentApi;
import org.killbill.billing.payment.api.PaymentGatewayApi;
+import org.killbill.billing.payment.caching.StateMachineConfigCache;
import org.killbill.billing.payment.core.PaymentExecutors;
-import org.killbill.billing.payment.core.PaymentProcessor;
import org.killbill.billing.payment.core.PaymentMethodProcessor;
+import org.killbill.billing.payment.core.PaymentProcessor;
import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
import org.killbill.billing.payment.dao.PaymentDao;
+import org.killbill.billing.payment.glue.PaymentModule;
import org.killbill.billing.payment.glue.TestPaymentModuleWithEmbeddedDB;
import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
@@ -47,6 +50,8 @@ import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
+import static org.killbill.billing.payment.provider.MockPaymentControlProviderPlugin.PLUGIN_NAME;
+
public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWithEmbeddedDB {
@Inject
@@ -66,6 +71,8 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
@Inject
protected PaymentApi paymentApi;
@Inject
+ protected AdminPaymentApi adminPaymentApi;
+ @Inject
protected PaymentGatewayApi paymentGatewayApi;
@Inject
protected AccountInternalApi accountApi;
@@ -79,6 +86,8 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
protected PaymentExecutors paymentExecutors;
@Inject
protected NonEntityDao nonEntityDao;
+ @Inject
+ protected StateMachineConfigCache stateMachineConfigCache;
@Override
protected KillbillConfigSource getConfigSource() {
@@ -96,11 +105,14 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
@BeforeMethod(groups = "slow")
public void beforeMethod() throws Exception {
super.beforeMethod();
+
+ stateMachineConfigCache.clearPaymentStateMachineConfig(PLUGIN_NAME, internalCallContext);
+ stateMachineConfigCache.loadDefaultPaymentStateMachineConfig(PaymentModule.DEFAULT_STATE_MACHINE_PAYMENT_XML);
+
paymentExecutors.initialize();
eventBus.start();
Profiling.resetPerThreadProfilingData();
clock.resetDeltaFromReality();
-
}
@AfterMethod(groups = "slow")
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
index d6fb5f3..1f81aeb 100644
--- a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
@@ -21,6 +21,7 @@ package org.killbill.billing.payment.provider;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -49,7 +50,7 @@ import org.killbill.billing.util.entity.DefaultPagination;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.clock.Clock;
-import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
@@ -241,6 +242,30 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
}
}
+ public void overridePaymentPluginStatus(final UUID kbPaymentId, final UUID kbTransactionId, final PaymentPluginStatus status) {
+ final List<PaymentTransactionInfoPlugin> existingTransactions = paymentTransactions.remove(kbPaymentId.toString());
+ final List<PaymentTransactionInfoPlugin> newTransactions = new LinkedList<PaymentTransactionInfoPlugin>();
+ paymentTransactions.put(kbPaymentId.toString(), newTransactions);
+
+ for (final PaymentTransactionInfoPlugin existingTransaction : existingTransactions) {
+ if (existingTransaction.getKbTransactionPaymentId().equals(kbTransactionId)) {
+ final PaymentTransactionInfoPlugin newTransaction = new DefaultNoOpPaymentInfoPlugin(existingTransaction.getKbPaymentId(),
+ existingTransaction.getKbTransactionPaymentId(),
+ existingTransaction.getTransactionType(),
+ existingTransaction.getAmount(),
+ existingTransaction.getCurrency(),
+ existingTransaction.getEffectiveDate(),
+ existingTransaction.getCreatedDate(),
+ status,
+ existingTransaction.getGatewayErrorCode(),
+ existingTransaction.getGatewayError());
+ newTransactions.add(newTransaction);
+ } else {
+ newTransactions.add(existingTransaction);
+ }
+ }
+ }
+
@Override
public PaymentTransactionInfoPlugin authorizePayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable<PluginProperty> properties, final CallContext context)
throws PaymentPluginApiException {
@@ -364,6 +389,21 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.REFUND, refundAmount, currency, properties);
}
+ public void overridePaymentTransactionPluginResult(final UUID kbPaymentId, final UUID kbTransactionId, final PaymentPluginStatus paymentPluginStatus) throws PaymentPluginApiException {
+ final List<PaymentTransactionInfoPlugin> existingTransactions = paymentTransactions.get(kbPaymentId.toString());
+ PaymentTransactionInfoPlugin paymentTransactionInfoPlugin = null;
+ for (final PaymentTransactionInfoPlugin existingTransaction : existingTransactions) {
+ if (existingTransaction.getKbTransactionPaymentId().equals(kbTransactionId)) {
+ paymentTransactionInfoPlugin = existingTransaction;
+ break;
+ }
+ }
+ Preconditions.checkNotNull(paymentTransactionInfoPlugin);
+
+ final Iterable<PluginProperty> pluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, paymentPluginStatus.toString(), false));
+ getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.AUTHORIZE, paymentTransactionInfoPlugin.getAmount(), paymentTransactionInfoPlugin.getCurrency(), pluginProperties);
+ }
+
private PaymentTransactionInfoPlugin getPaymentTransactionInfoPluginResult(final UUID kbPaymentId, final UUID kbTransactionId, final TransactionType type, @Nullable final BigDecimal amount, @Nullable final Currency currency, final Iterable<PluginProperty> pluginProperties) throws PaymentPluginApiException {
if (makePluginWaitSomeMilliseconds.get() > 0) {
try {
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 aad5f13..8051f80 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
@@ -290,7 +290,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
// Artificially move the transaction status to UNKNOWN
final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.AUTHORIZE).toString();
testListener.pushExpectedEvent(NextEvent.PAYMENT_PLUGIN_ERROR);
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+ paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), null, payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
payment.getTransactions().get(0).getId(), TransactionStatus.UNKNOWN, requestedAmount, account.getCurrency(),
"foo", "bar", internalCallContext);
testListener.assertListenerStatus();
@@ -322,7 +322,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
// Artificially move the transaction status to UNKNOWN
final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.AUTHORIZE).toString();
testListener.pushExpectedEvent(NextEvent.PAYMENT_PLUGIN_ERROR);
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+ paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), null, payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
payment.getTransactions().get(0).getId(), TransactionStatus.UNKNOWN, requestedAmount, account.getCurrency(),
"foo", "bar", internalCallContext);
testListener.assertListenerStatus();
@@ -367,7 +367,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
// Artificially move the transaction status to UNKNOWN
final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.AUTHORIZE).toString();
testListener.pushExpectedEvent(NextEvent.PAYMENT_PLUGIN_ERROR);
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+ paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), null, payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
payment.getTransactions().get(0).getId(), TransactionStatus.UNKNOWN, requestedAmount, account.getCurrency(),
"foo", "bar", internalCallContext);
testListener.assertListenerStatus();
@@ -400,7 +400,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
// Artificially move the transaction status to PENDING
final String paymentStateName = paymentSMHelper.getPendingStateForTransaction(TransactionType.AUTHORIZE).toString();
testListener.pushExpectedEvent(NextEvent.PAYMENT);
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+ paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), null, payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
payment.getTransactions().get(0).getId(), TransactionStatus.PENDING, requestedAmount, account.getCurrency(),
"loup", "chat", internalCallContext);
testListener.assertListenerStatus();
@@ -439,7 +439,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
testListener.pushExpectedEvent(NextEvent.PAYMENT);
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+ paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), null, payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
payment.getTransactions().get(0).getId(), TransactionStatus.PENDING, requestedAmount, account.getCurrency(),
"loup", "chat", internalCallContext);
testListener.assertListenerStatus();
diff --git a/payment/src/test/resources/org/killbill/billing/payment/PermissivePaymentStates.xml b/payment/src/test/resources/org/killbill/billing/payment/PermissivePaymentStates.xml
new file mode 100644
index 0000000..99d4bb9
--- /dev/null
+++ b/payment/src/test/resources/org/killbill/billing/payment/PermissivePaymentStates.xml
@@ -0,0 +1,541 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<stateMachineConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="StateMachineConfig.xsd">
+
+ <stateMachines>
+ <stateMachine name="BIG_BANG">
+ <states>
+ <state name="BIG_BANG_INIT"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>BIG_BANG_INIT</initialState>
+ <operation>OP_DUMMY</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>BIG_BANG_INIT</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_DUMMY"/>
+ </operations>
+ </stateMachine>
+ <stateMachine name="AUTHORIZE">
+ <states>
+ <state name="AUTH_INIT"/>
+ <state name="AUTH_PENDING"/>
+ <state name="AUTH_SUCCESS"/>
+ <state name="AUTH_FAILED"/>
+ <state name="AUTH_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>AUTH_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>AUTH_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>PENDING</operationResult>
+ <finalState>AUTH_PENDING</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_PENDING</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>AUTH_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_PENDING</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>AUTH_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_PENDING</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>AUTH_ERRORED</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>AUTH_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_AUTHORIZE"/>
+ </operations>
+ </stateMachine>
+
+ <stateMachine name="CAPTURE">
+ <states>
+ <state name="CAPTURE_INIT"/>
+ <state name="CAPTURE_PENDING"/>
+ <state name="CAPTURE_SUCCESS"/>
+ <state name="CAPTURE_FAILED"/>
+ <state name="CAPTURE_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>CAPTURE_INIT</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>CAPTURE_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>CAPTURE_INIT</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>CAPTURE_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>CAPTURE_INIT</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>PENDING</operationResult>
+ <finalState>CAPTURE_PENDING</finalState>
+ </transition>
+ <transition>
+ <initialState>CAPTURE_PENDING</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>CAPTURE_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>CAPTURE_PENDING</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>CAPTURE_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>CAPTURE_PENDING</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>CAPTURE_ERRORED</finalState>
+ </transition>
+ <transition>
+ <initialState>CAPTURE_INIT</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>CAPTURE_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_CAPTURE"/>
+ </operations>
+ </stateMachine>
+
+ <stateMachine name="PURCHASE">
+ <states>
+ <state name="PURCHASE_INIT"/>
+ <state name="PURCHASE_PENDING"/>
+ <state name="PURCHASE_SUCCESS"/>
+ <state name="PURCHASE_FAILED"/>
+ <state name="PURCHASE_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>PURCHASE_INIT</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>PURCHASE_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>PURCHASE_INIT</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>PURCHASE_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>PURCHASE_INIT</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>PENDING</operationResult>
+ <finalState>PURCHASE_PENDING</finalState>
+ </transition>
+ <transition>
+ <initialState>PURCHASE_PENDING</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>PURCHASE_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>PURCHASE_PENDING</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>PURCHASE_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>PURCHASE_PENDING</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>PURCHASE_ERRORED</finalState>
+ </transition>
+ <transition>
+ <initialState>PURCHASE_INIT</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>PURCHASE_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_PURCHASE"/>
+ </operations>
+ </stateMachine>
+
+ <stateMachine name="REFUND">
+ <states>
+ <state name="REFUND_INIT"/>
+ <state name="REFUND_PENDING"/>
+ <state name="REFUND_SUCCESS"/>
+ <state name="REFUND_FAILED"/>
+ <state name="REFUND_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>REFUND_INIT</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>REFUND_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>REFUND_INIT</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>REFUND_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>REFUND_INIT</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>PENDING</operationResult>
+ <finalState>REFUND_PENDING</finalState>
+ </transition>
+ <transition>
+ <initialState>REFUND_PENDING</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>REFUND_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>REFUND_PENDING</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>REFUND_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>REFUND_PENDING</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>REFUND_ERRORED</finalState>
+ </transition>
+ <transition>
+ <initialState>REFUND_INIT</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>REFUND_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_REFUND"/>
+ </operations>
+ </stateMachine>
+
+ <stateMachine name="CREDIT">
+ <states>
+ <state name="CREDIT_INIT"/>
+ <state name="CREDIT_PENDING"/>
+ <state name="CREDIT_SUCCESS"/>
+ <state name="CREDIT_FAILED"/>
+ <state name="CREDIT_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>CREDIT_INIT</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>CREDIT_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>CREDIT_INIT</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>CREDIT_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>CREDIT_INIT</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>PENDING</operationResult>
+ <finalState>CREDIT_PENDING</finalState>
+ </transition>
+ <transition>
+ <initialState>CREDIT_PENDING</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>CREDIT_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>CREDIT_PENDING</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>CREDIT_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>CREDIT_PENDING</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>CREDIT_ERRORED</finalState>
+ </transition>
+ <transition>
+ <initialState>CREDIT_INIT</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>CREDIT_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_CREDIT"/>
+ </operations>
+ </stateMachine>
+
+ <stateMachine name="VOID">
+ <states>
+ <state name="VOID_INIT"/>
+ <state name="VOID_PENDING"/>
+ <state name="VOID_SUCCESS"/>
+ <state name="VOID_FAILED"/>
+ <state name="VOID_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>VOID_INIT</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>VOID_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>VOID_INIT</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>VOID_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>VOID_INIT</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>PENDING</operationResult>
+ <finalState>VOID_PENDING</finalState>
+ </transition>
+ <transition>
+ <initialState>VOID_PENDING</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>VOID_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>VOID_PENDING</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>VOID_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>VOID_PENDING</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>VOID_ERRORED</finalState>
+ </transition>
+ <transition>
+ <initialState>VOID_INIT</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>VOID_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_VOID"/>
+ </operations>
+ </stateMachine>
+ <stateMachine name="CHARGEBACK">
+ <states>
+ <state name="CHARGEBACK_INIT"/>
+ <state name="CHARGEBACK_SUCCESS"/>
+ <state name="CHARGEBACK_FAILED"/>
+ <state name="CHARGEBACK_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>CHARGEBACK_INIT</initialState>
+ <operation>OP_CHARGEBACK</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>CHARGEBACK_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>CHARGEBACK_INIT</initialState>
+ <operation>OP_CHARGEBACK</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>CHARGEBACK_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>CHARGEBACK_INIT</initialState>
+ <operation>OP_CHARGEBACK</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>CHARGEBACK_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_CHARGEBACK"/>
+ </operations>
+ </stateMachine>
+ </stateMachines>
+
+ <linkStateMachines>
+ <linkStateMachine>
+ <initialStateMachine>BIG_BANG</initialStateMachine>
+ <initialState>BIG_BANG_INIT</initialState>
+ <finalStateMachine>AUTHORIZE</finalStateMachine>
+ <finalState>AUTH_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>BIG_BANG</initialStateMachine>
+ <initialState>BIG_BANG_INIT</initialState>
+ <finalStateMachine>PURCHASE</finalStateMachine>
+ <finalState>PURCHASE_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>BIG_BANG</initialStateMachine>
+ <initialState>BIG_BANG_INIT</initialState>
+ <finalStateMachine>CREDIT</finalStateMachine>
+ <finalState>CREDIT_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>AUTHORIZE</initialStateMachine>
+ <initialState>AUTH_SUCCESS</initialState>
+ <finalStateMachine>CAPTURE</finalStateMachine>
+ <finalState>CAPTURE_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>AUTHORIZE</initialStateMachine>
+ <initialState>AUTH_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>VOID</initialStateMachine>
+ <initialState>VOID_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>VOID</initialStateMachine>
+ <initialState>VOID_SUCCESS</initialState>
+ <finalStateMachine>CAPTURE</finalStateMachine>
+ <finalState>CAPTURE_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>VOID</initialStateMachine>
+ <initialState>VOID_SUCCESS</initialState>
+ <finalStateMachine>REFUND</finalStateMachine>
+ <finalState>REFUND_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>VOID</initialStateMachine>
+ <initialState>VOID_SUCCESS</initialState>
+ <finalStateMachine>CREDIT</finalStateMachine>
+ <finalState>CREDIT_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CAPTURE</initialStateMachine>
+ <initialState>CAPTURE_SUCCESS</initialState>
+ <finalStateMachine>REFUND</finalStateMachine>
+ <finalState>REFUND_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CAPTURE</initialStateMachine>
+ <initialState>CAPTURE_SUCCESS</initialState>
+ <finalStateMachine>CAPTURE</finalStateMachine>
+ <finalState>CAPTURE_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CAPTURE</initialStateMachine>
+ <initialState>CAPTURE_SUCCESS</initialState>
+ <finalStateMachine>CHARGEBACK</finalStateMachine>
+ <finalState>CHARGEBACK_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CAPTURE</initialStateMachine>
+ <initialState>CAPTURE_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>REFUND</initialStateMachine>
+ <initialState>REFUND_SUCCESS</initialState>
+ <finalStateMachine>REFUND</finalStateMachine>
+ <finalState>REFUND_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>REFUND</initialStateMachine>
+ <initialState>REFUND_SUCCESS</initialState>
+ <finalStateMachine>CHARGEBACK</finalStateMachine>
+ <finalState>CHARGEBACK_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>REFUND</initialStateMachine>
+ <initialState>REFUND_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>PURCHASE</initialStateMachine>
+ <initialState>PURCHASE_SUCCESS</initialState>
+ <finalStateMachine>REFUND</finalStateMachine>
+ <finalState>REFUND_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>PURCHASE</initialStateMachine>
+ <initialState>PURCHASE_SUCCESS</initialState>
+ <finalStateMachine>CHARGEBACK</finalStateMachine>
+ <finalState>CHARGEBACK_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CREDIT</initialStateMachine>
+ <initialState>CREDIT_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CHARGEBACK</initialStateMachine>
+ <initialState>CHARGEBACK_SUCCESS</initialState>
+ <finalStateMachine>CHARGEBACK</finalStateMachine>
+ <finalState>CHARGEBACK_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CHARGEBACK</initialStateMachine>
+ <initialState>CHARGEBACK_FAILED</initialState>
+ <finalStateMachine>REFUND</finalStateMachine>
+ <finalState>REFUND_INIT</finalState>
+ </linkStateMachine>
+ </linkStateMachines>
+</stateMachineConfig>
profiles/killbill/pom.xml 5(+5 -0)
diff --git a/profiles/killbill/pom.xml b/profiles/killbill/pom.xml
index 761ab45..f705cd3 100644
--- a/profiles/killbill/pom.xml
+++ b/profiles/killbill/pom.xml
@@ -95,6 +95,11 @@
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>com.jayway.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <scope>test</scope>
+ </dependency>
<!-- Make sure to only include this in tests (it contains mysqld for all platforms and is around 134M) -->
<dependency>
<groupId>mysql</groupId>
diff --git a/profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties b/profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties
index 8444c7d..182a482 100644
--- a/profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties
+++ b/profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties
@@ -1,231 +1,164 @@
## Top level keys
# general.notice = This notice should rarely, if ever, be used as everyone will see it
+### 0.17.x series ###
+
+# 0.17.0
+0.17.0.updates =
+0.17.0.notices = This is the latest dev release.
+0.17.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.0
+
+### 0.16.x series ###
+
+# 0.16.6
+0.16.6.updates =
+0.16.6.notices = This is the latest GA release.
+0.16.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.6
+
+# 0.16.5
+0.16.5.updates = 0.16.6
+0.16.5.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.5
+
+# 0.16.4
+0.16.4.updates = 0.16.6
+0.16.4.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.4
+
+# 0.16.3
+0.16.3.updates = 0.16.6
+0.16.3.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.3
+
+# 0.16.2
+0.16.2.updates = 0.16.6
+0.16.2.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.2
+
+# 0.16.1
+0.16.1.updates = 0.16.6
+0.16.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.1
+
+# 0.16.0
+0.16.0.updates = 0.16.6
+0.16.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.0
+
+### 0.15.x series ###
+
+# 0.15.10
+0.15.10.updates =
+0.15.10.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.10.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.10
+
+# 0.15.9
+0.15.9.updates = 0.15.10
+0.15.9.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.9.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.9
+
+# 0.15.8
+0.15.8.updates = 0.15.10
+0.15.8.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.8.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.8
+
+# 0.15.7
+0.15.7.updates = 0.15.10
+0.15.7.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.7.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.7
+
+# 0.15.6
+0.15.6.updates = 0.15.10
+0.15.6.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.6
+
+# 0.15.5
+0.15.5.updates = 0.15.10
+0.15.5.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.5
+
+# 0.15.4
+0.15.4.updates = 0.15.10
+0.15.4.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.4
+
+# 0.15.3
+0.15.3.updates = 0.15.10
+0.15.3.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.3
+
+# 0.15.2
+0.15.2.updates = 0.15.10
+0.15.2.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.2
+
+# 0.15.1
+0.15.1.updates = 0.15.10
+0.15.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.1
+
+# 0.15.0
+0.15.0.updates = 0.15.10
+0.15.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.0
+
### 0.14.x series ###
+# 0.14.1
+0.14.1.updates =
+0.14.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.14.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.14.1
+
# 0.14.0
-0.14.0.updates =
-0.14.0.notices = This is the latest GA release.
-0.14.0.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
+0.14.0.updates = 0.14.1
+0.14.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.14.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.14.0
### 0.13.x series ###
-## 0.13.7 -- latest unstable release
+# 0.13.7
0.13.7.updates =
-0.13.7.notices = This is the latest dev release.
+0.13.7.notices = We recommend upgrading to 0.16.6, our latest GA release.
0.13.7.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.7
-## 0.13.6
+# 0.13.6
0.13.6.updates = 0.13.7
-0.13.6.notices = We recommend upgrading to 0.13.7, our latest dev release.
+0.13.6.notices = We recommend upgrading to 0.16.6, our latest GA release.
0.13.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.6
-## 0.13.5
+# 0.13.5
0.13.5.updates = 0.13.7
-0.13.5.notices = We recommend upgrading to 0.13.7, our latest dev release.
+0.13.5.notices = We recommend upgrading to 0.16.6, our latest GA release.
0.13.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.5
-## 0.13.4
+# 0.13.4
0.13.4.updates = 0.13.7
-0.13.4.notices = We recommend upgrading to 0.13.7, our latest dev release.
+0.13.4.notices = We recommend upgrading to 0.16.6, our latest GA release.
0.13.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.4
-## 0.13.3
+# 0.13.3
0.13.3.updates = 0.13.7
-0.13.3.notices = We recommend upgrading to 0.13.7, our latest dev release.
+0.13.3.notices = We recommend upgrading to 0.16.6, our latest GA release.
0.13.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.3
-## 0.13.2
+# 0.13.2
0.13.2.updates = 0.13.7
-0.13.2.notices = We recommend upgrading to 0.13.7, our latest dev release.
+0.13.2.notices = We recommend upgrading to 0.16.6, our latest GA release.
0.13.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.2
-## 0.13.1
+# 0.13.1
0.13.1.updates = 0.13.7
-0.13.1.notices = We recommend upgrading to 0.13.7, our latest dev release.
-0.13.1.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
+0.13.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.1
### 0.12.x series ###
-## 0.12.1
+# 0.12.1
0.12.1.updates =
-0.12.1.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.12.1.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
+0.12.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.12.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.12.1
# 0.12.0
0.12.0.updates = 0.12.1
-0.12.0.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.12.0.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-### 0.11.x series ###
-
-## 0.11.13
-0.11.13.updates =
-0.11.13.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.13.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.12
-0.11.12.updates = 0.11.13
-0.11.12.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.12.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.11
-0.11.11.updates = 0.11.13
-0.11.11.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.11.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.10
-0.11.10.updates = 0.11.13
-0.11.10.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.10.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.9
-0.11.9.updates = 0.11.13
-0.11.9.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.9.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.8
-0.11.8.updates = 0.11.13
-0.11.8.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.8.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.7
-0.11.7.updates = 0.11.13
-0.11.7.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.7.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.6
-0.11.6.updates = 0.11.13
-0.11.6.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.6.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.5
-0.11.5.updates = 0.11.13
-0.11.5.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.5.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.4
-0.11.4.updates = 0.11.13
-0.11.4.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.4.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.3
-0.11.3.updates = 0.11.13
-0.11.3.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.3.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.2
-0.11.2.updates = 0.11.13
-0.11.2.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.2.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.1
-0.11.1.updates = 0.11.13
-0.11.1.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.1.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-### 0.10.x series ###
-
-## 0.10.2
-0.10.2.updates =
-0.10.2.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.10.2.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.10.1
-0.10.1.updates = 0.10.2
-0.10.1.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.10.1.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.10.0
-0.10.0.updates = 0.10.2
-0.10.0.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.10.0.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-### 0.9.x series ###
-
-## 0.9.2
-0.9.2.updates =
-0.9.2.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.9.2.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.9.1
-0.9.1.updates = 0.9.2
-0.9.1.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.9.1.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.9.0
-0.9.0.updates = 0.9.2
-0.9.0.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.9.0.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-### 0.8.x series ###
-
-## 0.8.13
-0.8.13.updates =
-0.8.13.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.13.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.12
-0.8.12.updates = 0.8.13
-0.8.12.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.12.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.11
-0.8.11.updates = 0.8.13
-0.8.11.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.11.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.10
-0.8.10.updates = 0.8.13
-0.8.10.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.10.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.9
-0.8.9.updates = 0.8.13
-0.8.9.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.9.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.8
-0.8.8.updates = 0.8.13
-0.8.8.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.8.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.7
-0.8.7.updates = 0.8.13
-0.8.7.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.7.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.6
-0.8.6.updates = 0.8.13
-0.8.6.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.6.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.5
-0.8.5.updates = 0.8.13
-0.8.5.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.5.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.4
-0.8.4.updates = 0.8.13
-0.8.4.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.4.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.3
-0.8.3.updates = 0.8.13
-0.8.3.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.3.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.2
-0.8.2.updates = 0.8.13
-0.8.2.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.2.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.1
-0.8.1.updates = 0.8.13
-0.8.1.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.1.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.8.0
-0.8.0.updates = 0.8.13
-0.8.0.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.8.0.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
+0.12.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.12.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.12.0
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java
index b2fb830..e2f0d13 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java
@@ -30,6 +30,7 @@ import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.client.KillBillClient;
import org.killbill.billing.client.KillBillHttpClient;
+import org.killbill.billing.client.RequestOptions;
import org.killbill.billing.client.model.Account;
import org.killbill.billing.client.model.PaymentMethod;
import org.killbill.billing.client.model.PaymentMethodPluginDetail;
@@ -59,6 +60,12 @@ public abstract class KillbillClient extends GuicyKillbillTestSuiteWithEmbeddedD
protected static final String reason = "i am god";
protected static final String comment = "no comment";
+ protected static RequestOptions requestOptions = RequestOptions.builder()
+ .withCreatedBy(createdBy)
+ .withReason(reason)
+ .withComment(comment)
+ .build();
+
protected KillBillClient killBillClient;
protected KillBillHttpClient killBillHttpClient;
@@ -119,7 +126,7 @@ public abstract class KillbillClient extends GuicyKillbillTestSuiteWithEmbeddedD
input.setBillingPeriod(billingPeriod);
input.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
- return killBillClient.createSubscription(input, waitCompletion ? DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC : -1, createdBy, reason, comment);
+ return killBillClient.createSubscription(input, null, waitCompletion ? DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC : -1, basicRequestOptions());
}
protected Account createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice() throws Exception {
@@ -189,4 +196,15 @@ public abstract class KillbillClient extends GuicyKillbillTestSuiteWithEmbeddedD
protected void crappyWaitForLackOfProperSynchonization(int sleepValueMSec) throws Exception {
Thread.sleep(sleepValueMSec);
}
+
+ /**
+ * Return a RequestOptions instance with the createdBy, reason and comment fields populated
+ * @return an instance of RequestOptions
+ */
+ protected RequestOptions basicRequestOptions() {
+ return RequestOptions.builder()
+ .withCreatedBy(createdBy)
+ .withReason(reason)
+ .withComment(comment).build();
+ }
}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAccount.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAccount.java
index 1f4f461..7b8c419 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAccount.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAccount.java
@@ -43,6 +43,7 @@ import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
import org.killbill.billing.util.api.AuditLevel;
import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -66,6 +67,10 @@ public class TestAccount extends TestJaxrsBase {
public void beforeMethod() throws Exception {
super.beforeMethod();
mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(PLUGIN_NAME);
+ }
+
+ @AfterMethod(groups = "slow")
+ public void tearDown() throws Exception {
mockPaymentProviderPlugin.clear();
}
@@ -405,7 +410,7 @@ public class TestAccount extends TestJaxrsBase {
final Account childAccount2 = killBillClient.createAccount(childInput2, createdBy, reason, comment);
// Retrieves children accounts by parent account id
- final Accounts childrenAccounts = killBillClient.getChildrenAccounts(parentAccount.getAccountId(), true, true);
+ final Accounts childrenAccounts = killBillClient.getChildrenAccounts(parentAccount.getAccountId(), true, true, requestOptions);
Assert.assertEquals(childrenAccounts.size(), 2);
Assert.assertTrue(childrenAccounts.get(0).equals(childAccount));
@@ -416,7 +421,7 @@ public class TestAccount extends TestJaxrsBase {
public void testEmptyGetChildrenAccounts() throws Exception {
// Retrieves children accounts by parent account id
- final Accounts childrenAccounts = killBillClient.getChildrenAccounts(UUID.randomUUID(), false, false);
+ final Accounts childrenAccounts = killBillClient.getChildrenAccounts(UUID.randomUUID(), false, false, requestOptions);
Assert.assertEquals(childrenAccounts.size(), 0);
}
@@ -425,9 +430,9 @@ public class TestAccount extends TestJaxrsBase {
public void testGetChildrenAccountsByNullId() throws Exception {
// Retrieves children accounts by parent account id
- final Accounts childrenAccounts = killBillClient.getChildrenAccounts(null, true, true);
+ final Accounts childrenAccounts = killBillClient.getChildrenAccounts(null, true, true, requestOptions);
Assert.assertEquals(childrenAccounts.size(), 0);
}
-}
\ No newline at end of file
+}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAdmin.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAdmin.java
index 5953157..288e8bd 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAdmin.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAdmin.java
@@ -49,7 +49,7 @@ public class TestAdmin extends TestJaxrsBase {
authTransaction.setPaymentExternalKey(paymentExternalKey);
authTransaction.setTransactionExternalKey(authTransactionExternalKey);
authTransaction.setTransactionType("AUTHORIZE");
- final Payment authPayment = killBillClient.createPayment(account.getAccountId(), account.getPaymentMethodId(), authTransaction, createdBy, reason, comment);
+ final Payment authPayment = killBillClient.createPayment(account.getAccountId(), account.getPaymentMethodId(), authTransaction, basicRequestOptions());
// First fix transactionStatus and paymentSstate (but not lastSuccessPaymentState
// Note that state is not consistent between TransactionStatus and lastSuccessPaymentState but we don't care.
@@ -88,7 +88,7 @@ public class TestAdmin extends TestJaxrsBase {
captureTransaction.setPaymentExternalKey(payment.getPaymentExternalKey());
captureTransaction.setTransactionExternalKey(capture1TransactionExternalKey);
try {
- killBillClient.captureAuthorization(captureTransaction, createdBy, reason, comment);
+ killBillClient.captureAuthorization(captureTransaction, basicRequestOptions());
if (expectException) {
Assert.fail("Capture should not succeed, after auth was moved to a PAYMENT_FAILURE");
}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestBuildResponse.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestBuildResponse.java
new file mode 100644
index 0000000..1b54361
--- /dev/null
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestBuildResponse.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 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.jaxrs;
+
+import org.killbill.billing.jaxrs.resources.AccountResource;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.server.log.ServerTestSuiteNoDB;
+import org.killbill.billing.util.config.definition.JaxrsConfig;
+import org.testng.annotations.Test;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.net.URI;
+import java.util.UUID;
+
+import com.sun.jersey.api.client.ClientResponse.Status;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertEqualsNoOrder;
+
+public class TestBuildResponse extends ServerTestSuiteNoDB {
+
+ @Test(groups = "fast", description = "Tests Uri Builder with Path Like URL and root Location")
+ public void testUriBuilderWithPathLikeUrlAndRoot() throws Exception {
+ UUID objectId = UUID.randomUUID();
+
+ final UriInfo uriInfo = mock(UriInfo.class);
+ URI uri = URI.create("http://localhost:8080");
+ when(uriInfo.getBaseUri()).thenReturn(uri);
+
+ JaxrsConfig jaxrsConfig = mock(JaxrsConfig.class);
+ when(jaxrsConfig.isJaxrsLocationFullUrl()).thenReturn(false);
+ JaxrsUriBuilder uriBuilder = new JaxrsUriBuilder(jaxrsConfig);
+ Response response = uriBuilder.buildResponse(uriInfo, AccountResource.class, "getAccount", objectId);
+
+ assertEquals(response.getStatus(), Status.CREATED.getStatusCode());
+ assertEquals(response.getMetadata().get("Location").get(0), "/1.0/kb/accounts/" + objectId.toString());
+ }
+
+ @Test(groups = "fast", description = "Tests Uri Builder with Path Like URL and non root Location")
+ public void testUriBuilderWithPathLikeUrlAndNonRoot() throws Exception {
+ UUID objectId = UUID.randomUUID();
+
+ final UriInfo uriInfo = mock(UriInfo.class);
+ URI uri = URI.create("http://localhost:8080/killbill");
+ when(uriInfo.getBaseUri()).thenReturn(uri);
+
+ JaxrsConfig jaxrsConfig = mock(JaxrsConfig.class);
+ when(jaxrsConfig.isJaxrsLocationFullUrl()).thenReturn(false);
+ JaxrsUriBuilder uriBuilder = new JaxrsUriBuilder(jaxrsConfig);
+ Response response = uriBuilder.buildResponse(uriInfo, AccountResource.class, "getAccount", objectId);
+
+ assertEquals(response.getStatus(), Status.CREATED.getStatusCode());
+ assertEquals(response.getMetadata().get("Location").get(0), "/killbill/1.0/kb/accounts/" + objectId.toString());
+ }
+
+ @Test(groups = "fast", description = "Tests Uri Builder with Full URL and root Location")
+ public void testUriBuilderWithoutPathLikeUrlAndRoot() throws Exception {
+ UUID objectId = UUID.randomUUID();
+
+ final UriInfo uriInfo = mock(UriInfo.class);
+ URI uri = URI.create("http://localhost:8080");
+ when(uriInfo.getBaseUri()).thenReturn(uri);
+ when(uriInfo.getAbsolutePath()).thenReturn(uri);
+
+ JaxrsConfig jaxrsConfig = mock(JaxrsConfig.class);
+ when(jaxrsConfig.isJaxrsLocationFullUrl()).thenReturn(true);
+ JaxrsUriBuilder uriBuilder = new JaxrsUriBuilder(jaxrsConfig);
+ Response response = uriBuilder.buildResponse(uriInfo, AccountResource.class, "getAccount", objectId);
+
+ assertEquals(response.getStatus(), Status.CREATED.getStatusCode());
+ assertEquals(response.getMetadata().get("Location").get(0).toString(), uri.toString() + "/1.0/kb/accounts/" + objectId.toString());
+ }
+
+ @Test(groups = "fast", description = "Tests Uri Builder with Full URL and non root Location")
+ public void testUriBuilderWithoutPathLikeUrlAndNonRoot() throws Exception {
+ UUID objectId = UUID.randomUUID();
+
+ final UriInfo uriInfo = mock(UriInfo.class);
+ URI uri = URI.create("http://localhost:8080/killbill");
+ when(uriInfo.getBaseUri()).thenReturn(uri);
+ when(uriInfo.getAbsolutePath()).thenReturn(uri);
+
+ JaxrsConfig jaxrsConfig = mock(JaxrsConfig.class);
+ when(jaxrsConfig.isJaxrsLocationFullUrl()).thenReturn(true);
+ JaxrsUriBuilder uriBuilder = new JaxrsUriBuilder(jaxrsConfig);
+ Response response = uriBuilder.buildResponse(uriInfo, AccountResource.class, "getAccount", objectId);
+
+ assertEquals(response.getStatus(), Status.CREATED.getStatusCode());
+ assertEquals(response.getMetadata().get("Location").get(0).toString(), uri.toString() + "/1.0/kb/accounts/" + objectId.toString());
+ }
+}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestBundle.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestBundle.java
index 7a1f6af..33e7df6 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestBundle.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestBundle.java
@@ -30,7 +30,6 @@ import org.killbill.billing.client.model.BlockingState;
import org.killbill.billing.client.model.BlockingStates;
import org.killbill.billing.client.model.Bundle;
import org.killbill.billing.client.model.Bundles;
-import org.killbill.billing.client.model.PluginProperty;
import org.killbill.billing.client.model.Subscription;
import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
@@ -120,7 +119,6 @@ public class TestBundle extends TestJaxrsBase {
assertEquals(newBundle.getAccountId(), newAccount.getAccountId());
}
-
@Test(groups = "slow", description = "Block a bundle")
public void testBlockBundle() throws Exception {
final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
@@ -133,7 +131,7 @@ public class TestBundle extends TestJaxrsBase {
final String bundleExternalKey = "93199";
final Subscription entitlement = createEntitlement(accountJson.getAccountId(), bundleExternalKey, productName,
- ProductCategory.BASE, term, true);
+ ProductCategory.BASE, term, true);
final Bundle bundle = killBillClient.getBundle(bundleExternalKey);
assertEquals(bundle.getAccountId(), accountJson.getAccountId());
@@ -153,20 +151,16 @@ public class TestBundle extends TestJaxrsBase {
final Subscription subscription2 = killBillClient.getSubscription(entitlement.getSubscriptionId());
assertEquals(subscription2.getState(), EntitlementState.ACTIVE);
- final BlockingStates blockingStates = killBillClient.getBlockingStates(accountJson.getAccountId(), null, ImmutableList.<String>of("service"), AuditLevel.FULL);
+ final BlockingStates blockingStates = killBillClient.getBlockingStates(accountJson.getAccountId(), null, ImmutableList.<String>of("service"), AuditLevel.FULL, basicRequestOptions());
Assert.assertEquals(blockingStates.size(), 2);
-
- final BlockingStates blockingStates2 = killBillClient.getBlockingStates(accountJson.getAccountId(), ImmutableList.<BlockingStateType>of(BlockingStateType.SUBSCRIPTION_BUNDLE), null, AuditLevel.FULL);
+ final BlockingStates blockingStates2 = killBillClient.getBlockingStates(accountJson.getAccountId(), ImmutableList.<BlockingStateType>of(BlockingStateType.SUBSCRIPTION_BUNDLE), null, AuditLevel.FULL, basicRequestOptions());
Assert.assertEquals(blockingStates2.size(), 2);
-
- final BlockingStates blockingStates3 = killBillClient.getBlockingStates(accountJson.getAccountId(), null, null, AuditLevel.FULL);
+ final BlockingStates blockingStates3 = killBillClient.getBlockingStates(accountJson.getAccountId(), null, null, AuditLevel.FULL, basicRequestOptions());
Assert.assertEquals(blockingStates3.size(), 3);
}
-
-
@Test(groups = "slow", description = "Can paginate and search through all bundles")
public void testBundlesPagination() throws Exception {
final Account accountJson = createAccount();
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
index 080d812..feac496 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
@@ -31,7 +31,6 @@ import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
-import org.killbill.billing.client.KillBillClientException;
import org.killbill.billing.client.model.Account;
import org.killbill.billing.client.model.Bundle;
import org.killbill.billing.client.model.Invoice;
@@ -42,6 +41,8 @@ import org.killbill.billing.util.api.AuditLevel;
import org.testng.Assert;
import org.testng.annotations.Test;
+import com.ning.http.client.Response;
+
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
@@ -66,6 +67,16 @@ public class TestEntitlement extends TestJaxrsBase {
// Retrieves with GET
Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ Assert.assertEquals(objFromJson.getPriceOverrides().size(), 2);
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(0).getFixedPrice(), BigDecimal.ZERO);
+ Assert.assertNull(objFromJson.getPriceOverrides().get(0).getRecurringPrice());
+
+ Assert.assertNull(objFromJson.getPriceOverrides().get(1).getFixedPrice());
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(1).getRecurringPrice(), new BigDecimal("249.95"));
+
+ // Equality in java client is not correctly implemented so manually check PriceOverrides section and then reset before equality
+ objFromJson.setPriceOverrides(null);
+ entitlementJson.setPriceOverrides(null);
Assert.assertTrue(objFromJson.equals(entitlementJson));
// Change plan IMM
@@ -111,6 +122,10 @@ public class TestEntitlement extends TestJaxrsBase {
// Retrieves with GET
Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ // Equality in java client is not correctly implemented so manually check PriceOverrides section and then reset before equality
+ objFromJson.setPriceOverrides(null);
+ entitlementJson.setPriceOverrides(null);
+
Assert.assertTrue(objFromJson.equals(entitlementJson));
// MOVE AFTER TRIAL
@@ -167,6 +182,10 @@ public class TestEntitlement extends TestJaxrsBase {
// Retrieves with GET
Subscription objFromJson = killBillClient.getSubscription(subscriptionJson.getSubscriptionId());
+ // Equality in java client is not correctly implemented so manually check PriceOverrides section and then reset before equality
+ objFromJson.setPriceOverrides(null);
+ subscriptionJson.setPriceOverrides(null);
+
Assert.assertTrue(objFromJson.equals(subscriptionJson));
assertEquals(objFromJson.getBillingPeriod(), BillingPeriod.ANNUAL);
@@ -204,7 +223,7 @@ public class TestEntitlement extends TestJaxrsBase {
overrides.add(new PhasePriceOverride(null, PhaseType.TRIAL.toString(), BigDecimal.TEN, null));
input.setPriceOverrides(overrides);
- final Subscription subscription = killBillClient.createSubscription(input, DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC, createdBy, reason, comment);
+ final Subscription subscription = killBillClient.createSubscription(input, null, DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC, basicRequestOptions());
final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, false, false, AuditLevel.FULL);
assertEquals(invoices.size(), 1);
@@ -279,4 +298,40 @@ public class TestEntitlement extends TestJaxrsBase {
final Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
Assert.assertTrue(objFromJson.equals(entitlementJson));
}
+
+ @Test(groups = "slow", description = "Verify we can move the BCD associated with the subscription")
+ public void testMoveEntitlementBCD() throws Exception {
+ final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account accountJson = createAccountWithDefaultPaymentMethod();
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+
+ final Subscription entitlementJson = createEntitlement(accountJson.getAccountId(), "99999", productName,
+ ProductCategory.BASE, term, true);
+
+ Assert.assertEquals(entitlementJson.getBillCycleDayLocal(), new Integer(25));
+
+ final Subscription updatedSubscription = new Subscription();
+ updatedSubscription.setSubscriptionId(entitlementJson.getSubscriptionId());
+ updatedSubscription.setBillCycleDayLocal(9);
+ killBillClient.updateSubscriptionBCD(updatedSubscription, null, DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC, basicRequestOptions());
+
+
+ final Subscription result = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ // Still shows as the 4 (BCD did not take effect)
+ Assert.assertEquals(result.getBillCycleDayLocal(), new Integer(25));
+
+ // 2012, 5, 9
+ clock.addDays(14);
+ crappyWaitForLackOfProperSynchonization();
+
+ final Subscription result2 = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ // Still shows as the 4 (BCD did not take effect)
+ Assert.assertEquals(result2.getBillCycleDayLocal(), new Integer(9));
+ }
+
+
}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
index deaf3fc..133777d 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
@@ -28,7 +28,6 @@ import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.client.model.Account;
import org.killbill.billing.client.model.AuditLog;
@@ -697,13 +696,12 @@ public class TestInvoice extends TestJaxrsBase {
final Account accountWithBalance = killBillClient.getAccount(accountJson.getAccountId(), true, true);
- final Invoice migrationInvoice = killBillClient.createMigrationInvoice(accountJson.getAccountId(), null, ImmutableList.<InvoiceItem>of(externalCharge), createdBy, reason, comment);
+ final Invoice migrationInvoice = killBillClient.createMigrationInvoice(accountJson.getAccountId(), null, ImmutableList.<InvoiceItem>of(externalCharge), basicRequestOptions());
assertEquals(migrationInvoice.getBalance(), BigDecimal.ZERO);
assertEquals(migrationInvoice.getItems().size(), 1);
assertEquals(migrationInvoice.getItems().get(0).getAmount().compareTo(chargeAmount), 0);
assertEquals(migrationInvoice.getItems().get(0).getCurrency(), accountJson.getCurrency());
-
final List<Invoice> invoicesWithMigration = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, true);
assertEquals(invoicesWithMigration.size(), 3);
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
index 7184aae..a37015b 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
@@ -37,6 +37,7 @@ import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
import org.killbill.billing.api.TestApiListener;
import org.killbill.billing.client.KillBillClient;
import org.killbill.billing.client.KillBillHttpClient;
+import org.killbill.billing.client.RequestOptions;
import org.killbill.billing.client.model.Payment;
import org.killbill.billing.client.model.PaymentTransaction;
import org.killbill.billing.client.model.Tenant;
@@ -247,6 +248,7 @@ public class TestJaxrsBase extends KillbillClient {
server.start();
}
+
protected Iterable<EventListener> getListeners() {
return new Iterable<EventListener>() {
@Override
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
index 69a716d..5c21ce8 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
@@ -43,6 +43,7 @@ import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
import org.killbill.billing.payment.provider.MockPaymentControlProviderPlugin;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -69,7 +70,6 @@ public class TestPayment extends TestJaxrsBase {
public void beforeMethod() throws Exception {
super.beforeMethod();
mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(PLUGIN_NAME);
- mockPaymentProviderPlugin.clear();
mockPaymentControlProviderPlugin = new MockPaymentControlProviderPlugin();
controlPluginRegistry.registerService(new OSGIServiceDescriptor() {
@@ -90,6 +90,11 @@ public class TestPayment extends TestJaxrsBase {
}, mockPaymentControlProviderPlugin);
}
+ @AfterMethod(groups = "slow")
+ public void tearDown() throws Exception {
+ mockPaymentProviderPlugin.clear();
+ }
+
@Test(groups = "slow")
public void testWithFailedPayment() throws Exception {
final Account account = createAccountWithDefaultPaymentMethod();
@@ -100,12 +105,13 @@ public class TestPayment extends TestJaxrsBase {
authTransaction.setAmount(BigDecimal.ONE);
authTransaction.setCurrency(account.getCurrency());
authTransaction.setTransactionType(TransactionType.AUTHORIZE.name());
- try {
- killBillClient.createPayment(account.getAccountId(), account.getPaymentMethodId(), authTransaction, ImmutableMap.<String, String>of(), createdBy, reason, comment);
- fail();
- } catch (KillBillClientException e) {
- assertEquals(402, e.getResponse().getStatusCode());
- }
+
+ final Payment payment = killBillClient.createPayment(account.getAccountId(), account.getPaymentMethodId(), authTransaction,
+ ImmutableMap.<String, String>of(), basicRequestOptions());
+ final PaymentTransaction paymentTransaction = payment.getTransactions().get(0);
+ assertEquals(paymentTransaction.getStatus(), TransactionStatus.PAYMENT_FAILURE.toString());
+ assertEquals(paymentTransaction.getGatewayErrorCode(), MockPaymentProviderPlugin.GATEWAY_ERROR_CODE);
+ assertEquals(paymentTransaction.getGatewayErrorMsg(), MockPaymentProviderPlugin.GATEWAY_ERROR);
}
@Test(groups = "slow")
@@ -118,12 +124,9 @@ public class TestPayment extends TestJaxrsBase {
authTransaction.setAmount(BigDecimal.ONE);
authTransaction.setCurrency(account.getCurrency());
authTransaction.setTransactionType(TransactionType.AUTHORIZE.name());
- try {
- killBillClient.createPayment(account.getAccountId(), account.getPaymentMethodId(), authTransaction, ImmutableMap.<String, String>of(), createdBy, reason, comment);
- fail();
- } catch (KillBillClientException e) {
- assertEquals(502, e.getResponse().getStatusCode());
- }
+ final Payment payment = killBillClient.createPayment(account.getAccountId(), account.getPaymentMethodId(), authTransaction, ImmutableMap.<String, String>of(), basicRequestOptions());
+ final PaymentTransaction paymentTransaction = payment.getTransactions().get(0);
+ assertEquals(paymentTransaction.getStatus(), TransactionStatus.PLUGIN_FAILURE.toString());
}
@Test(groups = "slow")
@@ -137,7 +140,7 @@ public class TestPayment extends TestJaxrsBase {
authTransaction.setCurrency(account.getCurrency());
authTransaction.setTransactionType(TransactionType.AUTHORIZE.name());
try {
- killBillClient.createPayment(account.getAccountId(), account.getPaymentMethodId(), authTransaction, ImmutableMap.<String, String>of(), createdBy, reason, comment);
+ killBillClient.createPayment(account.getAccountId(), account.getPaymentMethodId(), authTransaction, ImmutableMap.<String, String>of(), basicRequestOptions());
fail();
} catch (KillBillClientException e) {
assertEquals(504, e.getResponse().getStatusCode());
@@ -180,27 +183,27 @@ public class TestPayment extends TestJaxrsBase {
// Complete operation: first, only specify the payment id
final PaymentTransaction completeTransactionByPaymentId = new PaymentTransaction();
completeTransactionByPaymentId.setPaymentId(initialPayment.getPaymentId());
- final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentId, pluginProperties, createdBy, reason, comment);
+ final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentId, pluginProperties, basicRequestOptions());
verifyPayment(account, paymentMethodId, completedPaymentByPaymentId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), pending, amount, authAmount, BigDecimal.ZERO, BigDecimal.ZERO, 1, paymentNb);
// Second, only specify the payment external key
final PaymentTransaction completeTransactionByPaymentExternalKey = new PaymentTransaction();
completeTransactionByPaymentExternalKey.setPaymentExternalKey(initialPayment.getPaymentExternalKey());
- final Payment completedPaymentByExternalKey = killBillClient.completePayment(completeTransactionByPaymentExternalKey, pluginProperties, createdBy, reason, comment);
+ final Payment completedPaymentByExternalKey = killBillClient.completePayment(completeTransactionByPaymentExternalKey, pluginProperties, basicRequestOptions());
verifyPayment(account, paymentMethodId, completedPaymentByExternalKey, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), pending, amount, authAmount, BigDecimal.ZERO, BigDecimal.ZERO, 1, paymentNb);
// Third, specify the payment id and transaction external key
final PaymentTransaction completeTransactionWithTypeAndKey = new PaymentTransaction();
completeTransactionWithTypeAndKey.setPaymentId(initialPayment.getPaymentId());
completeTransactionWithTypeAndKey.setTransactionExternalKey(authPaymentTransaction.getTransactionExternalKey());
- final Payment completedPaymentByTypeAndKey = killBillClient.completePayment(completeTransactionWithTypeAndKey, pluginProperties, createdBy, reason, comment);
+ final Payment completedPaymentByTypeAndKey = killBillClient.completePayment(completeTransactionWithTypeAndKey, pluginProperties, basicRequestOptions());
verifyPayment(account, paymentMethodId, completedPaymentByTypeAndKey, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), pending, amount, authAmount, BigDecimal.ZERO, BigDecimal.ZERO, 1, paymentNb);
// Finally, specify the payment id and transaction id
final PaymentTransaction completeTransactionWithTypeAndId = new PaymentTransaction();
completeTransactionWithTypeAndId.setPaymentId(initialPayment.getPaymentId());
completeTransactionWithTypeAndId.setTransactionId(authPaymentTransaction.getTransactionId());
- final Payment completedPaymentByTypeAndId = killBillClient.completePayment(completeTransactionWithTypeAndId, pluginProperties, createdBy, reason, comment);
+ final Payment completedPaymentByTypeAndId = killBillClient.completePayment(completeTransactionWithTypeAndId, pluginProperties, basicRequestOptions());
verifyPayment(account, paymentMethodId, completedPaymentByTypeAndId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), pending, amount, authAmount, BigDecimal.ZERO, BigDecimal.ZERO, 1, paymentNb);
}
}
@@ -225,7 +228,7 @@ public class TestPayment extends TestJaxrsBase {
// Complete operation: first, only specify the payment id
final PaymentTransaction completeTransactionByPaymentId = new PaymentTransaction();
completeTransactionByPaymentId.setPaymentId(initialPayment.getPaymentId());
- final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentId, pluginProperties, createdBy, reason, comment);
+ final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentId, pluginProperties, basicRequestOptions());
verifyPayment(account, paymentMethodId, completedPaymentByPaymentId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), TransactionStatus.SUCCESS.name(), amount, amount, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
}
@@ -252,7 +255,7 @@ public class TestPayment extends TestJaxrsBase {
completeTransactionByPaymentIdAndInvalidTransactionId.setPaymentId(initialPayment.getPaymentId());
completeTransactionByPaymentIdAndInvalidTransactionId.setTransactionId(UUID.randomUUID());
try {
- killBillClient.completePayment(completeTransactionByPaymentIdAndInvalidTransactionId, pluginProperties, createdBy, reason, comment);
+ killBillClient.completePayment(completeTransactionByPaymentIdAndInvalidTransactionId, pluginProperties, basicRequestOptions());
fail("Payment completion should fail when invalid transaction id has been provided" );
} catch (final KillBillClientException expected) {
}
@@ -260,7 +263,7 @@ public class TestPayment extends TestJaxrsBase {
final PaymentTransaction completeTransactionByPaymentIdAndTransactionId = new PaymentTransaction();
completeTransactionByPaymentIdAndTransactionId.setPaymentId(initialPayment.getPaymentId());
completeTransactionByPaymentIdAndTransactionId.setTransactionId(initialPayment.getTransactions().get(0).getTransactionId());
- final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentIdAndTransactionId, pluginProperties, createdBy, reason, comment);
+ final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentIdAndTransactionId, pluginProperties, basicRequestOptions());
verifyPayment(account, paymentMethodId, completedPaymentByPaymentId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), TransactionStatus.SUCCESS.name(), amount, amount, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
}
@@ -285,7 +288,7 @@ public class TestPayment extends TestJaxrsBase {
completeTransactionByPaymentIdAndInvalidTransactionExternalKey.setPaymentId(initialPayment.getPaymentId());
completeTransactionByPaymentIdAndInvalidTransactionExternalKey.setTransactionExternalKey("bozo");
try {
- killBillClient.completePayment(completeTransactionByPaymentIdAndInvalidTransactionExternalKey, pluginProperties, createdBy, reason, comment);
+ killBillClient.completePayment(completeTransactionByPaymentIdAndInvalidTransactionExternalKey, pluginProperties, basicRequestOptions());
fail("Payment completion should fail when invalid transaction externalKey has been provided" );
} catch (final KillBillClientException expected) {
}
@@ -293,7 +296,7 @@ public class TestPayment extends TestJaxrsBase {
final PaymentTransaction completeTransactionByPaymentIdAndTransactionExternalKey = new PaymentTransaction();
completeTransactionByPaymentIdAndTransactionExternalKey.setPaymentId(initialPayment.getPaymentId());
completeTransactionByPaymentIdAndTransactionExternalKey.setTransactionExternalKey(authTransactionExternalKey);
- final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentIdAndTransactionExternalKey, pluginProperties, createdBy, reason, comment);
+ final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentIdAndTransactionExternalKey, pluginProperties, basicRequestOptions());
verifyPayment(account, paymentMethodId, completedPaymentByPaymentId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), TransactionStatus.SUCCESS.name(), amount, amount, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
}
@@ -320,7 +323,7 @@ public class TestPayment extends TestJaxrsBase {
completeTransactionByPaymentIdAndInvalidTransactionType.setPaymentId(initialPayment.getPaymentId());
completeTransactionByPaymentIdAndInvalidTransactionType.setTransactionType(TransactionType.CAPTURE.name());
try {
- killBillClient.completePayment(completeTransactionByPaymentIdAndInvalidTransactionType, pluginProperties, createdBy, reason, comment);
+ killBillClient.completePayment(completeTransactionByPaymentIdAndInvalidTransactionType, pluginProperties, basicRequestOptions());
fail("Payment completion should fail when invalid transaction type has been provided" );
} catch (final KillBillClientException expected) {
}
@@ -328,7 +331,7 @@ public class TestPayment extends TestJaxrsBase {
final PaymentTransaction completeTransactionByPaymentIdAndTransactionType = new PaymentTransaction();
completeTransactionByPaymentIdAndTransactionType.setPaymentId(initialPayment.getPaymentId());
completeTransactionByPaymentIdAndTransactionType.setTransactionType(transactionType.name());
- final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentIdAndTransactionType, pluginProperties, createdBy, reason, comment);
+ final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentIdAndTransactionType, pluginProperties, basicRequestOptions());
verifyPayment(account, paymentMethodId, completedPaymentByPaymentId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), TransactionStatus.SUCCESS.name(), amount, amount, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
}
@@ -353,7 +356,7 @@ public class TestPayment extends TestJaxrsBase {
final PaymentTransaction completeTransactionWithTypeAndKey = new PaymentTransaction();
completeTransactionWithTypeAndKey.setPaymentId(initialPayment.getPaymentId());
completeTransactionWithTypeAndKey.setTransactionExternalKey(authTransactionExternalKey);
- final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionWithTypeAndKey, pluginProperties, createdBy, reason, comment);
+ final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionWithTypeAndKey, pluginProperties, basicRequestOptions());
verifyPayment(account, paymentMethodId, completedPaymentByPaymentId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), TransactionStatus.SUCCESS.name(), amount, amount, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
}
@@ -376,7 +379,7 @@ public class TestPayment extends TestJaxrsBase {
final PaymentTransaction completeTransactionByPaymentId = new PaymentTransaction();
completeTransactionByPaymentId.setPaymentId(initialPayment.getPaymentId());
try {
- killBillClient.completePayment(completeTransactionByPaymentId, pluginProperties, createdBy, reason, comment);
+ killBillClient.completePayment(completeTransactionByPaymentId, pluginProperties, basicRequestOptions());
fail("Completion should not succeed, there is no PENDING payment transaction");
} catch (final KillBillClientException expected) {
// Invalid parameter paymentId: XXXX
@@ -406,21 +409,21 @@ public class TestPayment extends TestJaxrsBase {
refundTransaction.setTransactionExternalKey(refundTransactionExternalKey);
refundTransaction.setAmount(purchaseAmount);
refundTransaction.setCurrency(authPayment.getCurrency());
- final Payment refundPayment = killBillClient.refundPayment(refundTransaction, null, pluginProperties, createdBy, reason, comment);
+ final Payment refundPayment = killBillClient.refundPayment(refundTransaction, null, pluginProperties, basicRequestOptions());
verifyPaymentWithPendingRefund(account, paymentMethodId, paymentExternalKey, purchaseTransactionExternalKey, purchaseAmount, refundTransactionExternalKey, refundPayment);
final PaymentTransaction completeTransactionWithTypeAndKey = new PaymentTransaction();
completeTransactionWithTypeAndKey.setPaymentId(refundPayment.getPaymentId());
completeTransactionWithTypeAndKey.setTransactionExternalKey(refundTransactionExternalKey);
- final Payment completedPaymentByTypeAndKey = killBillClient.completePayment(completeTransactionWithTypeAndKey, pluginProperties, createdBy, reason, comment);
+ final Payment completedPaymentByTypeAndKey = killBillClient.completePayment(completeTransactionWithTypeAndKey, pluginProperties, basicRequestOptions());
verifyPaymentWithPendingRefund(account, paymentMethodId, paymentExternalKey, purchaseTransactionExternalKey, purchaseAmount, refundTransactionExternalKey, completedPaymentByTypeAndKey);
// Also, it should work if we specify the payment id and transaction id
final PaymentTransaction completeTransactionWithTypeAndId = new PaymentTransaction();
completeTransactionWithTypeAndId.setPaymentId(refundPayment.getPaymentId());
completeTransactionWithTypeAndId.setTransactionId(refundPayment.getTransactions().get(1).getTransactionId());
- final Payment completedPaymentByTypeAndId = killBillClient.completePayment(completeTransactionWithTypeAndId, pluginProperties, createdBy, reason, comment);
+ final Payment completedPaymentByTypeAndId = killBillClient.completePayment(completeTransactionWithTypeAndId, pluginProperties, basicRequestOptions());
verifyPaymentWithPendingRefund(account, paymentMethodId, paymentExternalKey, purchaseTransactionExternalKey, purchaseAmount, refundTransactionExternalKey, completedPaymentByTypeAndId);
}
@@ -432,12 +435,12 @@ public class TestPayment extends TestJaxrsBase {
final ComboPaymentTransaction comboPaymentTransaction = createComboPaymentTransaction(accountJson, paymentExternalKey);
- final Payment payment = killBillClient.createPayment(comboPaymentTransaction, ImmutableMap.<String, String>of(), createdBy, reason, comment);
+ final Payment payment = killBillClient.createPayment(comboPaymentTransaction, ImmutableMap.<String, String>of(), basicRequestOptions());
verifyComboPayment(payment, paymentExternalKey, BigDecimal.TEN, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
// Void payment using externalKey
final String voidTransactionExternalKey = UUID.randomUUID().toString();
- final Payment voidPayment = killBillClient.voidPayment(null, paymentExternalKey, voidTransactionExternalKey, null, ImmutableMap.<String, String>of(), createdBy, reason, comment);
+ final Payment voidPayment = killBillClient.voidPayment(null, paymentExternalKey, voidTransactionExternalKey, null, ImmutableMap.<String, String>of(), basicRequestOptions());
verifyPaymentTransaction(accountJson, voidPayment.getPaymentId(), paymentExternalKey, voidPayment.getTransactions().get(1),
voidTransactionExternalKey, null, "VOID", "SUCCESS");
}
@@ -451,7 +454,7 @@ public class TestPayment extends TestJaxrsBase {
mockPaymentControlProviderPlugin.setAborted(true);
try {
- killBillClient.createPayment(comboPaymentTransaction, Arrays.asList(MockPaymentControlProviderPlugin.PLUGIN_NAME), ImmutableMap.<String, String>of(), createdBy, reason, comment);
+ killBillClient.createPayment(comboPaymentTransaction, Arrays.asList(MockPaymentControlProviderPlugin.PLUGIN_NAME), ImmutableMap.<String, String>of(), basicRequestOptions());
fail();
} catch (KillBillClientException e) {
assertEquals(e.getResponse().getStatusCode(), 422);
@@ -469,7 +472,7 @@ public class TestPayment extends TestJaxrsBase {
mockPaymentControlProviderPlugin.throwsException(new IllegalStateException());
try {
- killBillClient.createPayment(comboPaymentTransaction, Arrays.asList(MockPaymentControlProviderPlugin.PLUGIN_NAME), ImmutableMap.<String, String>of(), createdBy, reason, comment);
+ killBillClient.createPayment(comboPaymentTransaction, Arrays.asList(MockPaymentControlProviderPlugin.PLUGIN_NAME), ImmutableMap.<String, String>of(), basicRequestOptions());
fail();
} catch (KillBillClientException e) {
assertEquals(e.getResponse().getStatusCode(), 500);
@@ -507,7 +510,7 @@ public class TestPayment extends TestJaxrsBase {
final ComboPaymentTransaction comboPaymentTransaction = new ComboPaymentTransaction(accountJson, paymentMethodJson, null, ImmutableList.<PluginProperty>of(), ImmutableList.<PluginProperty>of());
- final Payment payment = killBillClient.createPayment(comboPaymentTransaction, ImmutableMap.<String, String>of(), createdBy, reason, comment);
+ final Payment payment = killBillClient.createPayment(comboPaymentTransaction, ImmutableMap.<String, String>of(), basicRequestOptions());
// Client returns null in case of a 404
Assert.assertNull(payment);
}
@@ -528,7 +531,7 @@ public class TestPayment extends TestJaxrsBase {
captureTransaction.setPaymentExternalKey(paymentExternalKey);
captureTransaction.setTransactionExternalKey(capture1TransactionExternalKey);
// captureAuthorization is using paymentId
- final Payment capturedPayment1 = killBillClient.captureAuthorization(captureTransaction, createdBy, reason, comment);
+ final Payment capturedPayment1 = killBillClient.captureAuthorization(captureTransaction, basicRequestOptions());
verifyPayment(account, paymentMethodId, capturedPayment1, paymentExternalKey, authTransactionExternalKey, "AUTHORIZE", "SUCCESS",
BigDecimal.TEN, BigDecimal.TEN, BigDecimal.ONE, BigDecimal.ZERO, 2, paymentNb);
verifyPaymentTransaction(account, authPayment.getPaymentId(), paymentExternalKey, capturedPayment1.getTransactions().get(1),
@@ -539,7 +542,7 @@ public class TestPayment extends TestJaxrsBase {
captureTransaction.setTransactionExternalKey(capture2TransactionExternalKey);
// captureAuthorization is using externalKey
captureTransaction.setPaymentId(null);
- final Payment capturedPayment2 = killBillClient.captureAuthorization(captureTransaction, createdBy, reason, comment);
+ final Payment capturedPayment2 = killBillClient.captureAuthorization(captureTransaction, basicRequestOptions());
verifyPayment(account, paymentMethodId, capturedPayment2, paymentExternalKey, authTransactionExternalKey, "AUTHORIZE", "SUCCESS",
BigDecimal.TEN, BigDecimal.TEN, new BigDecimal("2"), BigDecimal.ZERO, 3, paymentNb);
verifyPaymentTransaction(account, authPayment.getPaymentId(), paymentExternalKey, capturedPayment2.getTransactions().get(2),
@@ -553,7 +556,7 @@ public class TestPayment extends TestJaxrsBase {
refundTransaction.setCurrency(account.getCurrency());
refundTransaction.setPaymentExternalKey(paymentExternalKey);
refundTransaction.setTransactionExternalKey(refundTransactionExternalKey);
- final Payment refundPayment = killBillClient.refundPayment(refundTransaction, createdBy, reason, comment);
+ final Payment refundPayment = killBillClient.refundPayment(refundTransaction, basicRequestOptions());
verifyPayment(account, paymentMethodId, refundPayment, paymentExternalKey, authTransactionExternalKey, "AUTHORIZE", "SUCCESS",
BigDecimal.TEN, BigDecimal.TEN, new BigDecimal("2"), new BigDecimal("2"), 4, paymentNb);
verifyPaymentTransaction(account, authPayment.getPaymentId(), paymentExternalKey, refundPayment.getTransactions().get(3),
@@ -578,7 +581,7 @@ public class TestPayment extends TestJaxrsBase {
authTransaction.setPaymentExternalKey(paymentExternalKey);
authTransaction.setTransactionExternalKey(transactionExternalKey);
authTransaction.setTransactionType(transactionType.toString());
- final Payment payment = killBillClient.createPayment(account.getAccountId(), paymentMethodId, authTransaction, pluginProperties, createdBy, reason, comment);
+ final Payment payment = killBillClient.createPayment(account.getAccountId(), paymentMethodId, authTransaction, pluginProperties, basicRequestOptions());
verifyPayment(account, paymentMethodId, payment, paymentExternalKey, transactionExternalKey, transactionType.toString(), transactionStatus, transactionAmount, authAmount, BigDecimal.ZERO, BigDecimal.ZERO, 1, paymentNb);
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPaymentGateway.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPaymentGateway.java
index 8da0baa..6ae89fb 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPaymentGateway.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPaymentGateway.java
@@ -42,7 +42,7 @@ public class TestPaymentGateway extends TestJaxrsBase {
final HostedPaymentPageFields hppFields = new HostedPaymentPageFields();
- final HostedPaymentPageFormDescriptor hostedPaymentPageFormDescriptor = killBillClient.buildFormDescriptor(hppFields, account.getAccountId(), null, ImmutableMap.<String, String>of(), createdBy, reason, comment);
+ final HostedPaymentPageFormDescriptor hostedPaymentPageFormDescriptor = killBillClient.buildFormDescriptor(hppFields, account.getAccountId(), null, ImmutableMap.<String, String>of(), basicRequestOptions());
Assert.assertEquals(hostedPaymentPageFormDescriptor.getKbAccountId(), account.getAccountId());
}
@@ -58,7 +58,7 @@ public class TestPaymentGateway extends TestJaxrsBase {
final ComboHostedPaymentPage comboHostedPaymentPage = new ComboHostedPaymentPage(account, paymentMethod, ImmutableList.<PluginProperty>of(), hppFields);
- final HostedPaymentPageFormDescriptor hostedPaymentPageFormDescriptor = killBillClient.buildFormDescriptor(comboHostedPaymentPage, ImmutableMap.<String, String>of(), createdBy, reason, comment);
+ final HostedPaymentPageFormDescriptor hostedPaymentPageFormDescriptor = killBillClient.buildFormDescriptor(comboHostedPaymentPage, ImmutableMap.<String, String>of(), basicRequestOptions());
Assert.assertNotNull(hostedPaymentPageFormDescriptor.getKbAccountId());
}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPerTenantConfig.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPerTenantConfig.java
index d07d474..3cc2e00 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPerTenantConfig.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPerTenantConfig.java
@@ -25,6 +25,7 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.RequestOptions;
import org.killbill.billing.client.model.Account;
import org.killbill.billing.client.model.ComboPaymentTransaction;
import org.killbill.billing.client.model.Payment;
@@ -88,7 +89,7 @@ public class TestPerTenantConfig extends TestJaxrsBase {
perTenantProperties.put("org.killbill.payment.retry.days", "1,1,1");
final String perTenantConfig = mapper.writeValueAsString(perTenantProperties);
- final TenantKey tenantKey = killBillClient.postConfigurationPropertiesForTenant(perTenantConfig, createdBy, reason, comment);
+ final TenantKey tenantKey = killBillClient.postConfigurationPropertiesForTenant(perTenantConfig, basicRequestOptions());
final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
@@ -107,7 +108,7 @@ public class TestPerTenantConfig extends TestJaxrsBase {
//
// Now unregister special per tenant config and we the first retry occurs one day after (and still fails), it now sets a retry date of 8 days
//
- killBillClient.unregisterConfigurationForTenant(createdBy, reason, comment);
+ killBillClient.unregisterConfigurationForTenant(basicRequestOptions());
// org.killbill.tenant.broadcast.rate has been set to 1s
crappyWaitForLackOfProperSynchonization(2000);
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTenantKV.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTenantKV.java
index 54a51cc..f2539d8 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTenantKV.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTenantKV.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * 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
@@ -17,32 +17,151 @@
package org.killbill.billing.jaxrs;
+import java.math.BigDecimal;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.RequestOptions;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.ComboPaymentTransaction;
+import org.killbill.billing.client.model.Payment;
+import org.killbill.billing.client.model.PaymentMethod;
+import org.killbill.billing.client.model.PaymentMethodPluginDetail;
+import org.killbill.billing.client.model.PaymentTransaction;
+import org.killbill.billing.client.model.PluginProperty;
+import org.killbill.billing.client.model.Tenant;
import org.killbill.billing.client.model.TenantKey;
+import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.tenant.api.TenantKV;
import org.testng.Assert;
import org.testng.annotations.Test;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.io.Resources;
+import com.jayway.awaitility.Awaitility;
+import com.jayway.awaitility.Duration;
public class TestTenantKV extends TestJaxrsBase {
@Test(groups = "slow", description = "Upload and retrieve a per plugin config")
public void testPerTenantPluginConfig() throws Exception {
-
final String pluginName = "PLUGIN_FOO";
final String pluginPath = Resources.getResource("plugin.yml").getPath();
final TenantKey tenantKey0 = killBillClient.registerPluginConfigurationForTenant(pluginName, pluginPath, createdBy, reason, comment);
Assert.assertEquals(tenantKey0.getKey(), TenantKV.TenantKey.PLUGIN_CONFIG_.toString() + pluginName);
- final TenantKey tenantKey1 = killBillClient.getPluginConfigurationForTenant(pluginName);
+ final TenantKey tenantKey1 = killBillClient.getPluginConfigurationForTenant(pluginName);
Assert.assertEquals(tenantKey1.getKey(), TenantKV.TenantKey.PLUGIN_CONFIG_.toString() + pluginName);
Assert.assertEquals(tenantKey1.getValues().size(), 1);
killBillClient.unregisterPluginConfigurationForTenant(pluginName, createdBy, reason, comment);
- final TenantKey tenantKey2 = killBillClient.getPluginConfigurationForTenant(pluginName);
+ final TenantKey tenantKey2 = killBillClient.getPluginConfigurationForTenant(pluginName);
Assert.assertEquals(tenantKey2.getKey(), TenantKV.TenantKey.PLUGIN_CONFIG_.toString() + pluginName);
Assert.assertEquals(tenantKey2.getValues().size(), 0);
}
+ @Test(groups = "slow", description = "Upload and retrieve a per plugin payment state machine config")
+ public void testPerTenantPluginPaymentStateMachineConfig() throws Exception {
+ // Create another tenant - it will have a different state machine
+ final Tenant otherTenantWithDifferentStateMachine = new Tenant();
+ otherTenantWithDifferentStateMachine.setApiKey(UUID.randomUUID().toString());
+ otherTenantWithDifferentStateMachine.setApiSecret(UUID.randomUUID().toString());
+ killBillClient.createTenant(otherTenantWithDifferentStateMachine, requestOptions);
+ final RequestOptions requestOptionsOtherTenant = requestOptions.extend()
+ .withTenantApiKey(otherTenantWithDifferentStateMachine.getApiKey())
+ .withTenantApiSecret(otherTenantWithDifferentStateMachine.getApiSecret())
+ .build();
+
+ // Verify initial state
+ final TenantKey emptyTenantKey = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptions);
+ Assert.assertEquals(emptyTenantKey.getValues().size(), 0);
+ final TenantKey emptyTenantKeyOtherTenant = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptionsOtherTenant);
+ Assert.assertEquals(emptyTenantKeyOtherTenant.getValues().size(), 0);
+
+ final String stateMachineConfigPath = Resources.getResource("SimplePaymentStates.xml").getPath();
+ final TenantKey tenantKey0 = killBillClient.registerPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, stateMachineConfigPath, requestOptionsOtherTenant);
+ Assert.assertEquals(tenantKey0.getKey(), TenantKV.TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString() + PLUGIN_NAME);
+
+ // Verify only the other tenant has the new state machine
+ final TenantKey emptyTenantKey1 = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptions);
+ Assert.assertEquals(emptyTenantKey1.getValues().size(), 0);
+ final TenantKey tenantKey1OtherTenant = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptionsOtherTenant);
+ Assert.assertEquals(tenantKey1OtherTenant.getKey(), TenantKV.TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString() + PLUGIN_NAME);
+ Assert.assertEquals(tenantKey1OtherTenant.getValues().size(), 1);
+
+ // Create an auth in both tenant
+ final Payment payment = createComboPaymentTransaction(requestOptions);
+ final Payment paymentOtherTenant = createComboPaymentTransaction(requestOptionsOtherTenant);
+
+ // Void in the first tenant (allowed by the default state machine)
+ final Payment voidPayment = killBillClient.voidPayment(payment.getPaymentId(), payment.getPaymentExternalKey(), UUID.randomUUID().toString(), ImmutableList.<String>of(), ImmutableMap.<String, String>of(), requestOptions);
+ Assert.assertEquals(voidPayment.getTransactions().get(0).getStatus(), TransactionStatus.SUCCESS.toString());
+ Assert.assertEquals(voidPayment.getTransactions().get(1).getStatus(), TransactionStatus.SUCCESS.toString());
+
+ // Void in the other tenant (disallowed)
+ try {
+ killBillClient.voidPayment(paymentOtherTenant.getPaymentId(), paymentOtherTenant.getPaymentExternalKey(), UUID.randomUUID().toString(), ImmutableList.<String>of(), ImmutableMap.<String, String>of(), requestOptionsOtherTenant);
+ Assert.fail();
+ } catch (final KillBillClientException e) {
+ Assert.assertEquals((int) e.getBillingException().getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+
+ // Remove the custom state machine
+ killBillClient.unregisterPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptionsOtherTenant);
+ final TenantKey tenantKey2 = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptionsOtherTenant);
+ Assert.assertEquals(tenantKey2.getKey(), TenantKV.TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString() + PLUGIN_NAME);
+ Assert.assertEquals(tenantKey2.getValues().size(), 0);
+
+ final AtomicReference<Payment> voidPaymentOtherTenant2Ref = new AtomicReference<Payment>();
+ Awaitility.await()
+ .atMost(8, TimeUnit.SECONDS)
+ .pollInterval(Duration.TWO_SECONDS)
+ .until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ // The void should now go through
+ try {
+ final Payment voidPaymentOtherTenant2 = killBillClient.voidPayment(paymentOtherTenant.getPaymentId(), paymentOtherTenant.getPaymentExternalKey(), UUID.randomUUID().toString(), ImmutableList.<String>of(), ImmutableMap.<String, String>of(), requestOptionsOtherTenant);
+ voidPaymentOtherTenant2Ref.set(voidPaymentOtherTenant2);
+ return voidPaymentOtherTenant2 != null;
+ } catch (final KillBillClientException e) {
+ // Invalidation hasn't happened yet
+ return false;
+ }
+ }
+ });
+ Assert.assertEquals(voidPaymentOtherTenant2Ref.get().getTransactions().get(0).getStatus(), TransactionStatus.SUCCESS.toString());
+ Assert.assertEquals(voidPaymentOtherTenant2Ref.get().getTransactions().get(1).getStatus(), TransactionStatus.SUCCESS.toString());
+ }
+
+ private Payment createComboPaymentTransaction(final RequestOptions requestOptions) throws KillBillClientException {
+ final Account accountJson = getAccount();
+ accountJson.setAccountId(null);
+
+ final PaymentMethodPluginDetail info = new PaymentMethodPluginDetail();
+ info.setProperties(null);
+
+ final String paymentMethodExternalKey = UUID.randomUUID().toString();
+ final PaymentMethod paymentMethodJson = new PaymentMethod(null, paymentMethodExternalKey, null, true, PLUGIN_NAME, info);
+
+ final String authTransactionExternalKey = UUID.randomUUID().toString();
+ final PaymentTransaction authTransactionJson = new PaymentTransaction();
+ authTransactionJson.setAmount(BigDecimal.TEN);
+ authTransactionJson.setCurrency(accountJson.getCurrency());
+ authTransactionJson.setPaymentExternalKey(UUID.randomUUID().toString());
+ authTransactionJson.setTransactionExternalKey(authTransactionExternalKey);
+ authTransactionJson.setTransactionType("AUTHORIZE");
+
+ final ComboPaymentTransaction comboAuthorization = new ComboPaymentTransaction(accountJson, paymentMethodJson, authTransactionJson, ImmutableList.<PluginProperty>of(), ImmutableList.<PluginProperty>of());
+ final Payment payment = killBillClient.createPayment(comboAuthorization, ImmutableMap.<String, String>of(), requestOptions);
+ Assert.assertEquals(payment.getTransactions().get(0).getStatus(), TransactionStatus.SUCCESS.toString());
+
+ return payment;
+ }
}
diff --git a/profiles/killbill/src/test/resources/SimplePaymentStates.xml b/profiles/killbill/src/test/resources/SimplePaymentStates.xml
new file mode 100644
index 0000000..abf8e46
--- /dev/null
+++ b/profiles/killbill/src/test/resources/SimplePaymentStates.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright 2016 Groupon, Inc
+ ~ Copyright 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.
+ -->
+
+<stateMachineConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="StateMachineConfig.xsd">
+
+ <stateMachines>
+ <stateMachine name="BIG_BANG">
+ <states>
+ <state name="BIG_BANG_INIT"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>BIG_BANG_INIT</initialState>
+ <operation>OP_DUMMY</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>BIG_BANG_INIT</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_DUMMY"/>
+ </operations>
+ </stateMachine>
+ <stateMachine name="AUTHORIZE">
+ <states>
+ <state name="AUTH_INIT"/>
+ <state name="AUTH_SUCCESS"/>
+ <state name="AUTH_FAILED"/>
+ <state name="AUTH_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>AUTH_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>AUTH_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>AUTH_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_AUTHORIZE"/>
+ </operations>
+ </stateMachine>
+ </stateMachines>
+
+ <linkStateMachines>
+ <linkStateMachine>
+ <initialStateMachine>BIG_BANG</initialStateMachine>
+ <initialState>BIG_BANG_INIT</initialState>
+ <finalStateMachine>AUTHORIZE</finalStateMachine>
+ <finalState>AUTH_INIT</finalState>
+ </linkStateMachine>
+ </linkStateMachines>
+</stateMachineConfig>
diff --git a/profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties b/profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties
index f585321..182a482 100644
--- a/profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties
+++ b/profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties
@@ -1,105 +1,164 @@
## Top level keys
# general.notice = This notice should rarely, if ever, be used as everyone will see it
+### 0.17.x series ###
+
+# 0.17.0
+0.17.0.updates =
+0.17.0.notices = This is the latest dev release.
+0.17.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.0
+
+### 0.16.x series ###
+
+# 0.16.6
+0.16.6.updates =
+0.16.6.notices = This is the latest GA release.
+0.16.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.6
+
+# 0.16.5
+0.16.5.updates = 0.16.6
+0.16.5.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.5
+
+# 0.16.4
+0.16.4.updates = 0.16.6
+0.16.4.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.4
+
+# 0.16.3
+0.16.3.updates = 0.16.6
+0.16.3.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.3
+
+# 0.16.2
+0.16.2.updates = 0.16.6
+0.16.2.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.2
+
+# 0.16.1
+0.16.1.updates = 0.16.6
+0.16.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.1
+
+# 0.16.0
+0.16.0.updates = 0.16.6
+0.16.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.0
+
+### 0.15.x series ###
+
+# 0.15.10
+0.15.10.updates =
+0.15.10.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.10.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.10
+
+# 0.15.9
+0.15.9.updates = 0.15.10
+0.15.9.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.9.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.9
+
+# 0.15.8
+0.15.8.updates = 0.15.10
+0.15.8.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.8.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.8
+
+# 0.15.7
+0.15.7.updates = 0.15.10
+0.15.7.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.7.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.7
+
+# 0.15.6
+0.15.6.updates = 0.15.10
+0.15.6.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.6
+
+# 0.15.5
+0.15.5.updates = 0.15.10
+0.15.5.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.5
+
+# 0.15.4
+0.15.4.updates = 0.15.10
+0.15.4.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.4
+
+# 0.15.3
+0.15.3.updates = 0.15.10
+0.15.3.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.3
+
+# 0.15.2
+0.15.2.updates = 0.15.10
+0.15.2.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.2
+
+# 0.15.1
+0.15.1.updates = 0.15.10
+0.15.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.1
+
+# 0.15.0
+0.15.0.updates = 0.15.10
+0.15.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.0
+
### 0.14.x series ###
+# 0.14.1
+0.14.1.updates =
+0.14.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.14.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.14.1
+
# 0.14.0
-0.14.0.updates =
-0.14.0.notices = This is the latest GA release.
-0.14.0.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
+0.14.0.updates = 0.14.1
+0.14.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.14.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.14.0
### 0.13.x series ###
-## 0.13.7 -- latest unstable release
+# 0.13.7
0.13.7.updates =
-0.13.7.notices = This is the latest dev release.
+0.13.7.notices = We recommend upgrading to 0.16.6, our latest GA release.
0.13.7.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.7
-## 0.13.6
+# 0.13.6
0.13.6.updates = 0.13.7
-0.13.6.notices = We recommend upgrading to 0.13.7, our latest dev release.
+0.13.6.notices = We recommend upgrading to 0.16.6, our latest GA release.
0.13.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.6
-## 0.13.5
+# 0.13.5
0.13.5.updates = 0.13.7
-0.13.5.notices = We recommend upgrading to 0.13.7, our latest dev release.
+0.13.5.notices = We recommend upgrading to 0.16.6, our latest GA release.
0.13.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.5
-## 0.13.4
+# 0.13.4
0.13.4.updates = 0.13.7
-0.13.4.notices = We recommend upgrading to 0.13.7, our latest dev release.
+0.13.4.notices = We recommend upgrading to 0.16.6, our latest GA release.
0.13.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.4
-## 0.13.3
+# 0.13.3
0.13.3.updates = 0.13.7
-0.13.3.notices = We recommend upgrading to 0.13.7, our latest dev release.
+0.13.3.notices = We recommend upgrading to 0.16.6, our latest GA release.
0.13.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.3
-## 0.13.2
+# 0.13.2
0.13.2.updates = 0.13.7
-0.13.2.notices = We recommend upgrading to 0.13.7, our latest dev release.
+0.13.2.notices = We recommend upgrading to 0.16.6, our latest GA release.
0.13.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.2
-## 0.13.1
+# 0.13.1
0.13.1.updates = 0.13.7
-0.13.1.notices = We recommend upgrading to 0.13.7, our latest dev release.
-0.13.1.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
+0.13.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.1
### 0.12.x series ###
-## 0.12.1
+# 0.12.1
0.12.1.updates =
-0.12.1.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.12.1.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
+0.12.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.12.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.12.1
# 0.12.0
0.12.0.updates = 0.12.1
-0.12.0.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.12.0.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-### 0.11.x series ###
-
-## 0.11.13
-0.11.13.updates =
-0.11.13.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.13.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.12
-0.11.12.updates = 0.11.13
-0.11.12.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.12.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.11
-0.11.11.updates = 0.11.13
-0.11.11.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.11.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.10
-0.11.10.updates = 0.11.13
-0.11.10.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.10.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.9
-0.11.9.updates = 0.11.13
-0.11.9.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.9.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.8
-0.11.8.updates = 0.11.13
-0.11.8.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.8.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.7
-0.11.7.updates = 0.11.13
-0.11.7.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.7.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.6
-0.11.6.updates = 0.11.13
-0.11.6.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.6.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
-
-## 0.11.5
-0.11.5.updates = 0.11.13
-0.11.5.notices = We recommend upgrading to 0.14.0, our latest GA release.
-0.11.5.release-notes = https://github.com/killbill/killbill/blob/master/NEWS
+0.12.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.12.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.12.0
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index 5f4ef51..2e7a534 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -19,6 +19,8 @@
package org.killbill.billing.subscription.api.svcs;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -28,10 +30,13 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingAlignment;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
@@ -70,9 +75,11 @@ import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseServ
import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
-import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.bcd.BCDEvent;
+import org.killbill.billing.subscription.events.bcd.BCDEventData;
import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
import org.killbill.billing.util.UUIDs;
+import org.killbill.billing.util.bcd.BillCycleDayCalculator;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.TenantContext;
@@ -107,6 +114,20 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
private final NotificationQueueService notificationQueueService;
+ public static final Comparator<SubscriptionBase> SUBSCRIPTIONS_COMPARATOR = new Comparator<SubscriptionBase>() {
+
+ @Override
+ public int compare(final SubscriptionBase o1, final SubscriptionBase o2) {
+ if (o1.getCategory() == ProductCategory.BASE) {
+ return -1;
+ } else if (o2.getCategory() == ProductCategory.BASE) {
+ return 1;
+ } else {
+ return ((DefaultSubscriptionBase) o1).getAlignStartDate().compareTo(((DefaultSubscriptionBase) o2).getAlignStartDate());
+ }
+ }
+ };
+
@Inject
public DefaultSubscriptionInternalApi(final SubscriptionDao dao,
final DefaultSubscriptionBaseApiService apiService,
@@ -345,6 +366,8 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
if (result != null && !result.isEmpty()) {
outputSubscriptions.addAll(result);
}
+ Collections.sort(outputSubscriptions, DefaultSubscriptionInternalApi.SUBSCRIPTIONS_COMPARATOR);
+
return createSubscriptionsForApiUse(outputSubscriptions);
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
@@ -609,7 +632,15 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
final Iterable<SubscriptionBaseEvent> filteredEvents = Iterables.filter(events, new Predicate<SubscriptionBaseEvent>() {
@Override
public boolean apply(final SubscriptionBaseEvent input) {
- return (eventType == SubscriptionBaseTransitionType.PHASE && input.getType() == EventType.PHASE) || input.getType() != EventType.PHASE;
+ switch (input.getType()) {
+ case PHASE:
+ return eventType == SubscriptionBaseTransitionType.PHASE;
+ case BCD_UPDATE:
+ return eventType == SubscriptionBaseTransitionType.BCD_CHANGE;
+ case API_USER:
+ default:
+ return true;
+ }
}
});
final Map<UUID, DateTime> result = filteredEvents.iterator().hasNext() ? new HashMap<UUID, DateTime>() : ImmutableMap.<UUID, DateTime>of();
@@ -622,6 +653,58 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
return result;
}
+ @Override
+ public void updateBCD(final UUID subscriptionId, final int bcd, @Nullable final LocalDate effectiveFromDate, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException {
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) getSubscriptionFromId(subscriptionId, internalCallContext);
+ final DateTime effectiveDate = getEffectiveDateForNewBCD(bcd, effectiveFromDate, internalCallContext);
+ final BCDEvent bcdEvent = BCDEventData.createBCDEvent(subscription, effectiveDate, bcd);
+ dao.createBCDChangeEvent(subscription, bcdEvent, internalCallContext);
+ }
+
+ @Override
+ public int getDefaultBillCycleDayLocal(final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final PlanPhaseSpecifier planPhaseSpecifier, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal, final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException {
+
+ try {
+ final Catalog catalog = catalogService.getFullCatalog(context);
+ final BillingAlignment alignment = catalog.billingAlignment(planPhaseSpecifier, effectiveDate);
+ return BillCycleDayCalculator.calculateBcdForAlignment(subscription, baseSubscription, alignment, accountTimeZone, accountBillCycleDayLocal);
+ } catch (final CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
+ }
+ }
+
+ private DateTime getEffectiveDateForNewBCD(final int bcd, @Nullable final LocalDate effectiveFromDate, final InternalCallContext internalCallContext) {
+ if (internalCallContext.getAccountRecordId() == null) {
+ throw new IllegalStateException("Need to have a valid context with accountRecordId");
+ }
+
+ // Today as seen by this account
+ final LocalDate startDate = effectiveFromDate != null ? effectiveFromDate : internalCallContext.toLocalDate(clock.getUTCNow());
+
+ // We want to compute a LocalDate in account TZ which maps to the provided 'bcd' and then compute an effectiveDate for when that BCD_CHANGE event needs to be triggered
+ //
+ // There is a bit of complexity to make sure the date we chose exists (e.g: a BCD of 31 in a february month would not make sense).
+ final int currentDay = startDate.getDayOfMonth();
+ final int lastDayOfMonth = startDate.dayOfMonth().getMaximumValue();
+
+ final LocalDate requestedDate;
+ if (bcd < currentDay) {
+ final LocalDate startDatePlusOneMonth = startDate.plusMonths(1);
+ final int lastDayOfNextMonth = startDatePlusOneMonth.dayOfMonth().getMaximumValue();
+ final int originalBCDORLastDayOfMonth = bcd <= lastDayOfNextMonth ? bcd : lastDayOfNextMonth;
+ requestedDate = new LocalDate(startDatePlusOneMonth.getYear(), startDatePlusOneMonth.getMonthOfYear(), originalBCDORLastDayOfMonth);
+ } else if (bcd == currentDay) {
+ // will default to immediate event
+ requestedDate = null;
+ } else if (bcd <= lastDayOfMonth) {
+ requestedDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), bcd);
+ } else /* bcd > lastDayOfMonth && bcd > currentDay */{
+ requestedDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), lastDayOfMonth);
+ }
+ return requestedDate == null ? clock.getUTCNow() : internalCallContext.toUTCDateTime(requestedDate);
+ }
+
+
private DateTime getBundleStartDateWithSanity(final UUID bundleId, @Nullable final DefaultSubscriptionBase baseSubscription, final Plan plan,
final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException {
switch (plan.getProduct().getCategory()) {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java
index 436393f..f554eeb 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java
@@ -37,6 +37,7 @@ import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.bcd.BCDEvent;
import org.killbill.billing.subscription.events.phase.PhaseEvent;
import org.killbill.billing.subscription.events.user.ApiEvent;
import org.killbill.billing.subscription.events.user.ApiEventType;
@@ -85,6 +86,7 @@ public class DefaultSubscriptionBaseTimeline implements SubscriptionBaseTimeline
PhaseType phaseType = null;
String planName = null;
String planPhaseName = null;
+ Integer billCycleDayLocal = null;
ApiEventType apiType = null;
switch (cur.getType()) {
@@ -99,6 +101,11 @@ public class DefaultSubscriptionBaseTimeline implements SubscriptionBaseTimeline
priceListName = prevPriceListName;
break;
+ case BCD_UPDATE:
+ final BCDEvent bcdEvent = (BCDEvent) cur;
+ billCycleDayLocal = bcdEvent.getBillCycleDayLocal();
+ break;
+
case API_USER:
final ApiEvent userEV = (ApiEvent) cur;
apiType = userEV.getApiEventType();
@@ -116,6 +123,7 @@ public class DefaultSubscriptionBaseTimeline implements SubscriptionBaseTimeline
final String planNameWithClosure = planName;
final String planPhaseNameWithClosure = planPhaseName;
+ final Integer billCycleDayLocalWithClosure = billCycleDayLocal;
final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(productName, category, billingPeriod, priceListName, phaseType);
result.add(new ExistingEvent() {
@Override
@@ -147,6 +155,11 @@ public class DefaultSubscriptionBaseTimeline implements SubscriptionBaseTimeline
public String getPlanPhaseName() {
return planPhaseNameWithClosure;
}
+
+ @Override
+ public Integer getBillCycleDayLocal() {
+ return billCycleDayLocalWithClosure;
+ }
});
prevPlanName = planName;
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java
index aca165c..4e4d7b7 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java
@@ -42,10 +42,12 @@ public class DefaultEffectiveSubscriptionEvent extends DefaultSubscriptionEvent
@JsonProperty("previousPlan") final String previousPlan,
@JsonProperty("previousPhase") final String previousPhase,
@JsonProperty("previousPriceList") final String previousPriceList,
+ @JsonProperty("previousBillCycleDayLocal") final Integer previousBillCycleDayLocal,
@JsonProperty("nextState") final EntitlementState nextState,
@JsonProperty("nextPlan") final String nextPlan,
@JsonProperty("nextPhase") final String nextPhase,
@JsonProperty("nextPriceList") final String nextPriceList,
+ @JsonProperty("nextBillCycleDayLocal") final Integer nextBillCycleDayLocal,
@JsonProperty("totalOrdering") final Long totalOrdering,
@JsonProperty("transitionType") final SubscriptionBaseTransitionType transitionType,
@JsonProperty("remainingEventsForUserOperation") final Integer remainingEventsForUserOperation,
@@ -54,7 +56,7 @@ public class DefaultEffectiveSubscriptionEvent extends DefaultSubscriptionEvent
@JsonProperty("searchKey2") final Long searchKey2,
@JsonProperty("userToken") final UUID userToken) {
super(eventId, subscriptionId, bundleId, effectiveTransitionTime, effectiveTransitionTime, previousState, previousPlan,
- previousPhase, previousPriceList, nextState, nextPlan, nextPhase, nextPriceList, totalOrdering,
+ previousPhase, previousPriceList, previousBillCycleDayLocal, nextState, nextPlan, nextPhase, nextPriceList, nextBillCycleDayLocal, totalOrdering,
transitionType, remainingEventsForUserOperation, startDate, searchKey1, searchKey2, userToken);
}
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java
index 2c78684..1e918ef 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java
@@ -40,10 +40,12 @@ public class DefaultRequestedSubscriptionEvent extends DefaultSubscriptionEvent
@JsonProperty("previousPlan") final String previousPlan,
@JsonProperty("previousPhase") final String previousPhase,
@JsonProperty("previousPriceList") final String previousPriceList,
+ @JsonProperty("previousBillCycleDayLocal") final Integer previousBillCycleDayLocal,
@JsonProperty("nextState") final EntitlementState nextState,
@JsonProperty("nextPlan") final String nextPlan,
@JsonProperty("nextPhase") final String nextPhase,
@JsonProperty("nextPriceList") final String nextPriceList,
+ @JsonProperty("nextBillCycleDayLocal") final Integer nextBillCycleDayLocal,
@JsonProperty("totalOrdering") final Long totalOrdering,
@JsonProperty("transitionType") final SubscriptionBaseTransitionType transitionType,
@JsonProperty("remainingEventsForUserOperation") final Integer remainingEventsForUserOperation,
@@ -52,7 +54,7 @@ public class DefaultRequestedSubscriptionEvent extends DefaultSubscriptionEvent
@JsonProperty("searchKey2") final Long searchKey2,
@JsonProperty("userToken") final UUID userToken) {
super(eventId, subscriptionId, bundleId, requestedTransitionTime, effectiveTransitionTime, previousState, previousPlan,
- previousPhase, previousPriceList, nextState, nextPlan, nextPhase, nextPriceList, totalOrdering,
+ previousPhase, previousPriceList, previousBillCycleDayLocal, nextState, nextPlan, nextPhase, nextPriceList, nextBillCycleDayLocal, totalOrdering,
transitionType, remainingEventsForUserOperation, startDate, searchKey1, searchKey2, userToken);
}
@@ -63,6 +65,6 @@ public class DefaultRequestedSubscriptionEvent extends DefaultSubscriptionEvent
final Long searchKey2,
final UUID userToken) {
this(nextEvent.getId(), nextEvent.getSubscriptionId(), subscription.getBundleId(), nextEvent.getEffectiveDate(), nextEvent.getEffectiveDate(),
- null, null, null, null, null, null, null, null, nextEvent.getTotalOrdering(), transitionType, 0, null, searchKey1, searchKey2, userToken);
+ null, null, null, null, null, null, null, null, null, null, nextEvent.getTotalOrdering(), transitionType, 0, null, searchKey1, searchKey2, userToken);
}
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
index 9594dc5..f6c0e8a 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
@@ -31,10 +31,10 @@ import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
-import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.catalog.api.ProductCategory;
@@ -50,6 +50,7 @@ import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionDataIterator.Visibility;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.bcd.BCDEvent;
import org.killbill.billing.subscription.events.phase.PhaseEvent;
import org.killbill.billing.subscription.events.user.ApiEvent;
import org.killbill.billing.subscription.events.user.ApiEventType;
@@ -253,7 +254,7 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
@Override
public DateTime changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
- final List<PlanPhasePriceOverride> overrides, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
+ final List<PlanPhasePriceOverride> overrides, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
return apiService.changePlanWithPolicy(this, productName, term, priceList, overrides, policy, context);
}
@@ -350,6 +351,21 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
return category;
}
+ @Override
+ public Integer getBillCycleDayLocal() {
+
+ final SubscriptionBaseTransitionDataIterator it = new SubscriptionBaseTransitionDataIterator(
+ clock, transitions, Order.DESC_FROM_FUTURE, Kind.SUBSCRIPTION,
+ Visibility.FROM_DISK_ONLY, TimeLimit.PAST_OR_PRESENT_ONLY);
+ while (it.hasNext()) {
+ final SubscriptionBaseTransition cur = it.next();
+ if (cur.getTransitionType() == SubscriptionBaseTransitionType.BCD_CHANGE) {
+ return cur.getNextBillingCycleDayLocal();
+ }
+ }
+ return null;
+ }
+
public DateTime getBundleStartDate() {
return bundleStartDate;
}
@@ -408,6 +424,14 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
return true;
}
+ @Override
+ public DateTime getDateOfFirstRecurringNonZeroCharge() {
+ final Plan initialPlan = !transitions.isEmpty() ? transitions.get(0).getNextPlan() : null;
+ final PlanPhase initialPhase = !transitions.isEmpty() ? transitions.get(0).getNextPhase() : null;
+ final PhaseType initialPhaseType = initialPhase != null ? initialPhase.getPhaseType() : null;
+ return initialPlan.dateOfFirstRecurringNonZeroCharge(getStartDate(), initialPhaseType);
+ }
+
public SubscriptionBaseTransitionData getTransitionFromEvent(final SubscriptionBaseEvent event, final int seqId) {
if (transitions == null || event == null) {
return null;
@@ -502,7 +526,7 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
final DateTime candidateResult;
switch (policy) {
case IMMEDIATE:
- candidateResult = clock.getUTCNow();
+ candidateResult = clock.getUTCNow();
break;
case END_OF_TERM:
//
@@ -511,7 +535,7 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
// 1. account is not being invoiced, for e.g AUTO_INVOICING_OFF nis set
// 2. In the case if FIXED item CTD is set using startDate of the service period
//
- candidateResult = (chargedThroughDate != null && chargedThroughDate.isAfter(clock.getUTCNow())) ? chargedThroughDate : clock.getUTCNow();
+ candidateResult = (chargedThroughDate != null && chargedThroughDate.isAfter(clock.getUTCNow())) ? chargedThroughDate : clock.getUTCNow();
break;
default:
throw new SubscriptionBaseError(String.format(
@@ -558,6 +582,7 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
EntitlementState nextState = null;
String nextPlanName = null;
String nextPhaseName = null;
+ Integer nextBillingCycleDayLocal = null;
UUID prevEventId = null;
DateTime prevCreatedDate = null;
@@ -565,6 +590,7 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
PriceList previousPriceList = null;
Plan previousPlan = null;
PlanPhase previousPhase = null;
+ Integer previousBillingCycleDayLocal = null;
transitions = new LinkedList<SubscriptionBaseTransition>();
@@ -588,6 +614,11 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
nextPhaseName = phaseEV.getPhase();
break;
+ case BCD_UPDATE:
+ final BCDEvent bcdEvent = (BCDEvent) cur;
+ nextBillingCycleDayLocal = bcdEvent.getBillCycleDayLocal();
+ break;
+
case API_USER:
final ApiEvent userEV = (ApiEvent) cur;
apiEventType = userEV.getApiEventType();
@@ -641,9 +672,12 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
prevEventId, prevCreatedDate,
previousState, previousPlan, previousPhase,
previousPriceList,
+ previousBillingCycleDayLocal,
nextEventId, nextCreatedDate,
nextState, nextPlan, nextPhase,
- nextPriceList, cur.getTotalOrdering(),
+ nextPriceList,
+ nextBillingCycleDayLocal,
+ cur.getTotalOrdering(),
cur.getCreatedDate(),
nextUserToken,
isFromDisk);
@@ -656,7 +690,7 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
previousPriceList = nextPriceList;
prevEventId = nextEventId;
prevCreatedDate = nextCreatedDate;
-
+ previousBillingCycleDayLocal = nextBillingCycleDayLocal;
}
}
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java
index 04601ac..89f8aa7 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java
@@ -41,14 +41,17 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
private final String previousPriceList;
private final String previousPlan;
private final String previousPhase;
+ private final Integer previousBillCycleDayLocal;
private final EntitlementState nextState;
private final String nextPriceList;
private final String nextPlan;
private final String nextPhase;
+ private final Integer nextBillCycleDayLocal;
private final Integer remainingEventsForUserOperation;
private final SubscriptionBaseTransitionType transitionType;
private final DateTime startDate;
+
public DefaultSubscriptionEvent(final SubscriptionBaseTransitionData in, final DateTime startDate,
final Long searchKey1,
final Long searchKey2,
@@ -62,10 +65,12 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
(in.getPreviousPlan() != null) ? in.getPreviousPlan().getName() : null,
(in.getPreviousPhase() != null) ? in.getPreviousPhase().getName() : null,
(in.getPreviousPriceList() != null) ? in.getPreviousPriceList().getName() : null,
+ in.getPreviousBillingCycleDayLocal(),
in.getNextState(),
(in.getNextPlan() != null) ? in.getNextPlan().getName() : null,
(in.getNextPhase() != null) ? in.getNextPhase().getName() : null,
(in.getNextPriceList() != null) ? in.getNextPriceList().getName() : null,
+ in.getNextBillingCycleDayLocal(),
in.getTotalOrdering(),
in.getTransitionType(),
in.getRemainingEventsForUserOperation(),
@@ -85,10 +90,12 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
@JsonProperty("previousPlan") final String previousPlan,
@JsonProperty("previousPhase") final String previousPhase,
@JsonProperty("previousPriceList") final String previousPriceList,
+ @JsonProperty("previousBillCycleDayLocal") final Integer previousBillCycleDayLocal,
@JsonProperty("nextState") final EntitlementState nextState,
@JsonProperty("nextPlan") final String nextPlan,
@JsonProperty("nextPhase") final String nextPhase,
@JsonProperty("nextPriceList") final String nextPriceList,
+ @JsonProperty("nextBillCycleDayLocal") final Integer nextBillCycleDayLocal,
@JsonProperty("totalOrdering") final Long totalOrdering,
@JsonProperty("transitionType") final SubscriptionBaseTransitionType transitionType,
@JsonProperty("remainingEventsForUserOperation") final Integer remainingEventsForUserOperation,
@@ -104,11 +111,13 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
this.effectiveTransitionTime = effectiveTransitionTime;
this.previousState = previousState;
this.previousPriceList = previousPriceList;
+ this.previousBillCycleDayLocal = previousBillCycleDayLocal;
this.previousPlan = previousPlan;
this.previousPhase = previousPhase;
this.nextState = nextState;
this.nextPlan = nextPlan;
this.nextPriceList = nextPriceList;
+ this.nextBillCycleDayLocal = nextBillCycleDayLocal;
this.nextPhase = nextPhase;
this.totalOrdering = totalOrdering;
this.transitionType = transitionType;
@@ -154,6 +163,11 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
}
@Override
+ public Integer getPreviousBillCycleDayLocal() {
+ return previousBillCycleDayLocal;
+ }
+
+ @Override
public String getNextPlan() {
return nextPlan;
}
@@ -179,6 +193,11 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
}
@Override
+ public Integer getNextBillCycleDayLocal() {
+ return nextBillCycleDayLocal;
+ }
+
+ @Override
public Integer getRemainingEventsForUserOperation() {
return remainingEventsForUserOperation;
}
@@ -221,10 +240,12 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
sb.append(", effectiveTransitionTime=").append(effectiveTransitionTime);
sb.append(", previousState=").append(previousState);
sb.append(", previousPriceList='").append(previousPriceList).append('\'');
+ sb.append(", previousBillCycleDayLocal='").append(previousBillCycleDayLocal).append('\'');
sb.append(", previousPlan='").append(previousPlan).append('\'');
sb.append(", previousPhase='").append(previousPhase).append('\'');
sb.append(", nextState=").append(nextState);
sb.append(", nextPriceList='").append(nextPriceList).append('\'');
+ sb.append(", nextBillCycleDayLocal='").append(nextBillCycleDayLocal).append('\'');
sb.append(", nextPlan='").append(nextPlan).append('\'');
sb.append(", nextPhase='").append(nextPhase).append('\'');
sb.append(", remainingEventsForUserOperation=").append(remainingEventsForUserOperation);
@@ -263,6 +284,9 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
if (nextPriceList != null ? !nextPriceList.equals(that.nextPriceList) : that.nextPriceList != null) {
return false;
}
+ if (nextBillCycleDayLocal != null ? !nextBillCycleDayLocal.equals(that.nextBillCycleDayLocal) : that.nextBillCycleDayLocal != null) {
+ return false;
+ }
if (nextState != that.nextState) {
return false;
}
@@ -275,6 +299,9 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
if (previousPriceList != null ? !previousPriceList.equals(that.previousPriceList) : that.previousPriceList != null) {
return false;
}
+ if (previousBillCycleDayLocal != null ? !previousBillCycleDayLocal.equals(that.previousBillCycleDayLocal) : that.previousBillCycleDayLocal != null) {
+ return false;
+ }
if (previousState != that.previousState) {
return false;
}
@@ -309,10 +336,12 @@ public abstract class DefaultSubscriptionEvent extends BusEventBase implements S
result = 31 * result + (effectiveTransitionTime != null ? effectiveTransitionTime.hashCode() : 0);
result = 31 * result + (previousState != null ? previousState.hashCode() : 0);
result = 31 * result + (previousPriceList != null ? previousPriceList.hashCode() : 0);
+ result = 31 * result + (previousBillCycleDayLocal != null ? previousBillCycleDayLocal.hashCode() : 0);
result = 31 * result + (previousPlan != null ? previousPlan.hashCode() : 0);
result = 31 * result + (previousPhase != null ? previousPhase.hashCode() : 0);
result = 31 * result + (nextState != null ? nextState.hashCode() : 0);
result = 31 * result + (nextPriceList != null ? nextPriceList.hashCode() : 0);
+ result = 31 * result + (nextBillCycleDayLocal != null ? nextBillCycleDayLocal.hashCode() : 0);
result = 31 * result + (nextPlan != null ? nextPlan.hashCode() : 0);
result = 31 * result + (nextPhase != null ? nextPhase.hashCode() : 0);
result = 31 * result + (remainingEventsForUserOperation != null ? remainingEventsForUserOperation.hashCode() : 0);
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java
index df2ccad..511b142 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java
@@ -39,6 +39,7 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
private final DateTime effectiveTransitionTime;
private final EntitlementState previousState;
private final PriceList previousPriceList;
+ private final Integer previousBillingCycleDayLocal;
private final UUID previousEventId;
private final DateTime previousEventCreatedDate;
private final Plan previousPlan;
@@ -47,6 +48,7 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
private final DateTime nextEventCreatedDate;
private final EntitlementState nextState;
private final PriceList nextPriceList;
+ private final Integer nextBillingCycleDayLocal;
private final Plan nextPlan;
private final PlanPhase nextPhase;
private final Boolean isFromDisk;
@@ -66,12 +68,14 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
final Plan previousPlan,
final PlanPhase previousPhase,
final PriceList previousPriceList,
+ final Integer previousBillingCycleDayLocal,
final UUID nextEventId,
final DateTime nextEventCreatedDate,
final EntitlementState nextState,
final Plan nextPlan,
final PlanPhase nextPhase,
final PriceList nextPriceList,
+ final Integer nextBillingCycleDayLocal,
final Long totalOrdering,
final DateTime createdDate,
final UUID userToken,
@@ -84,11 +88,13 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
this.effectiveTransitionTime = effectiveTransitionTime;
this.previousState = previousState;
this.previousPriceList = previousPriceList;
+ this.previousBillingCycleDayLocal = previousBillingCycleDayLocal;
this.previousPlan = previousPlan;
this.previousPhase = previousPhase;
this.nextState = nextState;
this.nextPlan = nextPlan;
this.nextPriceList = nextPriceList;
+ this.nextBillingCycleDayLocal = nextBillingCycleDayLocal;
this.nextPhase = nextPhase;
this.totalOrdering = totalOrdering;
this.previousEventId = previousEventId;
@@ -118,6 +124,7 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
this.previousEventCreatedDate = input.getPreviousEventCreatedDate();
this.previousState = input.getPreviousState();
this.previousPriceList = input.getPreviousPriceList();
+ this.previousBillingCycleDayLocal = input.getPreviousBillingCycleDayLocal();
this.previousPlan = input.getPreviousPlan();
this.previousPhase = input.getPreviousPhase();
this.nextEventId = input.getNextEventId();
@@ -125,6 +132,7 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
this.nextState = input.getNextState();
this.nextPlan = input.getNextPlan();
this.nextPriceList = input.getNextPriceList();
+ this.nextBillingCycleDayLocal = input.getNextBillingCycleDayLocal();
this.nextPhase = input.getNextPhase();
this.totalOrdering = input.getTotalOrdering();
this.isFromDisk = input.isFromDisk();
@@ -208,6 +216,16 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
return nextPriceList;
}
+ @Override
+ public Integer getPreviousBillingCycleDayLocal() {
+ return previousBillingCycleDayLocal;
+ }
+
+ @Override
+ public Integer getNextBillingCycleDayLocal() {
+ return nextBillingCycleDayLocal;
+ }
+
public UUID getUserToken() {
return userToken;
}
@@ -227,6 +245,8 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
return apiEventType.getSubscriptionTransitionType();
case PHASE:
return SubscriptionBaseTransitionType.PHASE;
+ case BCD_UPDATE:
+ return SubscriptionBaseTransitionType.BCD_CHANGE;
default:
throw new SubscriptionBaseError("Unexpected event type " + eventType);
}
@@ -270,10 +290,12 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
sb.append(", effectiveTransitionTime=").append(effectiveTransitionTime);
sb.append(", previousState=").append(previousState);
sb.append(", previousPriceList=").append(previousPriceList);
+ sb.append(", previousBillingCycleDayLocal=").append(previousBillingCycleDayLocal);
sb.append(", previousPlan=").append(previousPlan);
sb.append(", previousPhase=").append(previousPhase);
sb.append(", nextState=").append(nextState);
sb.append(", nextPriceList=").append(nextPriceList);
+ sb.append(", nextBillingCycleDayLocal=").append(nextBillingCycleDayLocal);
sb.append(", nextPlan=").append(nextPlan);
sb.append(", nextPhase=").append(nextPhase);
sb.append(", isFromDisk=").append(isFromDisk);
@@ -321,6 +343,9 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
if (nextPriceList != null ? !nextPriceList.equals(that.nextPriceList) : that.nextPriceList != null) {
return false;
}
+ if (nextBillingCycleDayLocal != null ? !nextBillingCycleDayLocal.equals(that.nextBillingCycleDayLocal) : that.nextBillingCycleDayLocal != null) {
+ return false;
+ }
if (nextState != that.nextState) {
return false;
}
@@ -333,6 +358,9 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
if (previousPriceList != null ? !previousPriceList.equals(that.previousPriceList) : that.previousPriceList != null) {
return false;
}
+ if (previousBillingCycleDayLocal != null ? !previousBillingCycleDayLocal.equals(that.previousBillingCycleDayLocal) : that.previousBillingCycleDayLocal != null) {
+ return false;
+ }
if (previousState != that.previousState) {
return false;
}
@@ -348,7 +376,6 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
if (userToken != null ? !userToken.equals(that.userToken) : that.userToken != null) {
return false;
}
-
return true;
}
@@ -363,10 +390,12 @@ public class SubscriptionBaseTransitionData implements SubscriptionBaseTransitio
result = 31 * result + (effectiveTransitionTime != null ? effectiveTransitionTime.hashCode() : 0);
result = 31 * result + (previousState != null ? previousState.hashCode() : 0);
result = 31 * result + (previousPriceList != null ? previousPriceList.hashCode() : 0);
+ result = 31 * result + (previousBillingCycleDayLocal != null ? previousBillingCycleDayLocal.hashCode() : 0);
result = 31 * result + (previousPlan != null ? previousPlan.hashCode() : 0);
result = 31 * result + (previousPhase != null ? previousPhase.hashCode() : 0);
result = 31 * result + (nextState != null ? nextState.hashCode() : 0);
result = 31 * result + (nextPriceList != null ? nextPriceList.hashCode() : 0);
+ result = 31 * result + (nextBillingCycleDayLocal != null ? nextBillingCycleDayLocal.hashCode() : 0);
result = 31 * result + (nextPlan != null ? nextPlan.hashCode() : 0);
result = 31 * result + (nextPhase != null ? nextPhase.hashCode() : 0);
result = 31 * result + (isFromDisk != null ? isFromDisk.hashCode() : 0);
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
index bf05324..6b157c2 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
@@ -159,6 +159,8 @@ public class DefaultSubscriptionBaseService implements EventListener, Subscripti
} else if (event.getType() == EventType.API_USER && subscription.getCategory() == ProductCategory.BASE) {
final CallContext callContext = internalCallContextFactory.createCallContext(context);
eventSent = onBasePlanEvent(subscription, event, callContext);
+ } else if (event.getType() == EventType.BCD_UPDATE) {
+ eventSent = false;
}
if (!eventSent) {
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 cead31c..9b5b400 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
@@ -47,6 +47,7 @@ import org.killbill.billing.entitlement.api.SubscriptionApiException;
import org.killbill.billing.entity.EntityPersistenceException;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.svcs.DefaultSubscriptionInternalApi;
import org.killbill.billing.subscription.api.transfer.BundleTransferData;
import org.killbill.billing.subscription.api.transfer.SubscriptionTransferData;
import org.killbill.billing.subscription.api.transfer.TransferCancelData;
@@ -66,6 +67,8 @@ import org.killbill.billing.subscription.engine.dao.model.SubscriptionModelDao;
import org.killbill.billing.subscription.events.EventBaseBuilder;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.bcd.BCDEvent;
+import org.killbill.billing.subscription.events.bcd.BCDEventBuilder;
import org.killbill.billing.subscription.events.phase.PhaseEvent;
import org.killbill.billing.subscription.events.phase.PhaseEventBuilder;
import org.killbill.billing.subscription.events.user.ApiEvent;
@@ -622,7 +625,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
final SubscriptionEventSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
final UUID subscriptionId = subscription.getId();
- cancelFutureEventsFromTransaction(subscriptionId, changeEvents.get(0).getEffectiveDate(), entitySqlDaoWrapperFactory, context);
+ cancelFutureEventsFromTransaction(subscriptionId, changeEvents.get(0).getEffectiveDate(), entitySqlDaoWrapperFactory, false, context);
for (final SubscriptionBaseEvent cur : changeEvents) {
transactional.create(new SubscriptionEventModelDao(cur), context);
@@ -643,8 +646,6 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
});
}
-
-
private List<SubscriptionBaseEvent> filterSubscriptionBaseEvents(final List<SubscriptionEventModelDao> models) {
final Collection<SubscriptionEventModelDao> filteredModels = Collections2.filter(models, new Predicate<SubscriptionEventModelDao>() {
@Override
@@ -673,7 +674,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
private void cancelSubscriptionFromTransaction(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent cancelEvent, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context, final int seqId)
throws EntityPersistenceException {
final UUID subscriptionId = subscription.getId();
- cancelFutureEventsFromTransaction(subscriptionId, cancelEvent.getEffectiveDate(), entitySqlDaoWrapperFactory, context);
+ cancelFutureEventsFromTransaction(subscriptionId, cancelEvent.getEffectiveDate(), entitySqlDaoWrapperFactory, true, context);
entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class).create(new SubscriptionEventModelDao(cancelEvent), context);
final boolean isBusEvent = cancelEvent.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0;
@@ -687,10 +688,12 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
cancelFutureEventFromTransaction(subscriptionId, entitySqlDaoWrapperFactory, EventType.PHASE, null, context);
}
- private void cancelFutureEventsFromTransaction(final UUID subscriptionId, final DateTime effectiveDate, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) {
+ private void cancelFutureEventsFromTransaction(final UUID subscriptionId, final DateTime effectiveDate, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final boolean includingBCDChange, final InternalCallContext context) {
final List<SubscriptionEventModelDao> eventModels = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class).getFutureActiveEventForSubscription(subscriptionId.toString(), effectiveDate.toDate(), context);
for (final SubscriptionEventModelDao cur : eventModels) {
- unactivateEventFromTransaction(cur, entitySqlDaoWrapperFactory, context);
+ if (includingBCDChange || cur.getEventType() != EventType.BCD_UPDATE) {
+ unactivateEventFromTransaction(cur, entitySqlDaoWrapperFactory, context);
+ }
}
}
@@ -763,18 +766,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
}
// Make sure BasePlan -- if exists-- is first
- Collections.sort(input, new Comparator<SubscriptionBase>() {
- @Override
- public int compare(final SubscriptionBase o1, final SubscriptionBase o2) {
- if (o1.getCategory() == ProductCategory.BASE) {
- return -1;
- } else if (o2.getCategory() == ProductCategory.BASE) {
- return 1;
- } else {
- return ((DefaultSubscriptionBase) o1).getAlignStartDate().compareTo(((DefaultSubscriptionBase) o2).getAlignStartDate());
- }
- }
- });
+ Collections.sort(input, DefaultSubscriptionInternalApi.SUBSCRIPTIONS_COMPARATOR);
final List<ApiEventChange> baseChangeEvents = new LinkedList<ApiEventChange>();
ApiEventCancel baseCancellationEvent = null;
@@ -868,9 +860,20 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
// Set total ordering value of the fake dryRun event to make sure billing events are correctly ordered
final SubscriptionBaseEvent curAdjustedDryRun;
if (!events.isEmpty()) {
- final EventBaseBuilder eventBuilder = (curDryRun.getType() == EventType.API_USER) ?
- new ApiEventBuilder((ApiEvent) curDryRun) :
- new PhaseEventBuilder((PhaseEvent) curDryRun);
+
+ final EventBaseBuilder eventBuilder;
+ switch (curDryRun.getType()) {
+ case PHASE:
+ eventBuilder = new PhaseEventBuilder((PhaseEvent) curDryRun);
+ break;
+ case BCD_UPDATE:
+ eventBuilder = new BCDEventBuilder((BCDEvent) curDryRun);
+ break;
+ case API_USER:
+ default:
+ eventBuilder = new ApiEventBuilder((ApiEvent) curDryRun);
+ break;
+ }
eventBuilder.setTotalOrdering(events.get(events.size() - 1).getTotalOrdering() + 1);
curAdjustedDryRun = eventBuilder.build();
@@ -915,6 +918,25 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
});
}
+ @Override
+ public void createBCDChangeEvent(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent bcdEvent, final InternalCallContext context) {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+ final SubscriptionEventSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
+ transactional.create(new SubscriptionEventModelDao(bcdEvent), context);
+
+ // Notify the Bus
+ notifyBusOfRequestedChange(entitySqlDaoWrapperFactory, subscription, bcdEvent, SubscriptionBaseTransitionType.BCD_CHANGE, context);
+ final boolean isBusEvent = bcdEvent.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0;
+ recordBusOrFutureNotificationFromTransaction(subscription, bcdEvent, entitySqlDaoWrapperFactory, isBusEvent, 0, context);
+
+ return null;
+ }
+ });
+
+ }
+
private DefaultSubscriptionBase createSubscriptionForInternalUse(final SubscriptionBase shellSubscription, final List<SubscriptionBaseEvent> events, final InternalTenantContext context) throws CatalogApiException {
final DefaultSubscriptionBase result = new DefaultSubscriptionBase(new SubscriptionBuilder(((DefaultSubscriptionBase) shellSubscription)), null, clock);
if (events.size() > 0) {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
index 9bcdd69..9063bba 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
@@ -19,10 +19,11 @@ package org.killbill.billing.subscription.engine.dao.model;
import java.util.UUID;
import org.joda.time.DateTime;
-
+import org.killbill.billing.subscription.events.EventBaseBuilder;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
-import org.killbill.billing.subscription.events.EventBaseBuilder;
+import org.killbill.billing.subscription.events.bcd.BCDEvent;
+import org.killbill.billing.subscription.events.bcd.BCDEventBuilder;
import org.killbill.billing.subscription.events.phase.PhaseEvent;
import org.killbill.billing.subscription.events.phase.PhaseEventBuilder;
import org.killbill.billing.subscription.events.user.ApiEvent;
@@ -43,6 +44,7 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
private String planName;
private String phaseName;
private String priceListName;
+ private int billingCycleDayLocal;
private boolean isActive;
public SubscriptionEventModelDao() {
@@ -51,7 +53,7 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
public SubscriptionEventModelDao(final UUID id, final long totalOrdering, final EventType eventType, final ApiEventType userType,
final DateTime effectiveDate, final UUID subscriptionId,
- final String planName, final String phaseName, final String priceListName,
+ final String planName, final String phaseName, final String priceListName, final int billingCycleDayLocal,
final boolean active, final DateTime createDate, final DateTime updateDate) {
super(id, createDate, updateDate);
this.totalOrdering = totalOrdering;
@@ -62,6 +64,7 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
this.planName = planName;
this.phaseName = phaseName;
this.priceListName = priceListName;
+ this.billingCycleDayLocal = billingCycleDayLocal;
this.isActive = active;
}
@@ -73,8 +76,15 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
this.effectiveDate = src.getEffectiveDate();
this.subscriptionId = src.getSubscriptionId();
this.planName = eventType == EventType.API_USER ? ((ApiEvent) src).getEventPlan() : null;
- this.phaseName = eventType == EventType.API_USER ? ((ApiEvent) src).getEventPlanPhase() : ((PhaseEvent) src).getPhase();
+ if (eventType == EventType.API_USER) {
+ this.phaseName = ((ApiEvent) src).getEventPlanPhase();
+ } else if (eventType == EventType.PHASE) {
+ this.phaseName = ((PhaseEvent) src).getPhase();
+ } else {
+ this.phaseName = null;
+ }
this.priceListName = eventType == EventType.API_USER ? ((ApiEvent) src).getPriceList() : null;
+ this.billingCycleDayLocal = eventType == EventType.BCD_UPDATE ? ((BCDEvent) src).getBillCycleDayLocal() : 0;
this.isActive = src.isActive();
}
@@ -110,6 +120,10 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
return priceListName;
}
+ public int getBillingCycleDayLocal() {
+ return billingCycleDayLocal;
+ }
+
// TODO required for jdbi binder
public boolean getIsActive() {
return isActive;
@@ -151,6 +165,11 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
this.priceListName = priceListName;
}
+
+ public void setBillingCycleDayLocal(final int billingCycleDayLocal) {
+ this.billingCycleDayLocal = billingCycleDayLocal;
+ }
+
public void setIsActive(final boolean isActive) {
this.isActive = isActive;
}
@@ -161,16 +180,21 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
return null;
}
- final EventBaseBuilder<?> base = ((src.getEventType() == EventType.PHASE) ?
- new PhaseEventBuilder() :
- new ApiEventBuilder())
- .setTotalOrdering(src.getTotalOrdering())
- .setUuid(src.getId())
- .setSubscriptionId(src.getSubscriptionId())
- .setCreatedDate(src.getCreatedDate())
- .setUpdatedDate(src.getUpdatedDate())
- .setEffectiveDate(src.getEffectiveDate())
- .setActive(src.isActive());
+ final EventBaseBuilder<?> base;
+ if (src.getEventType() == EventType.PHASE) {
+ base = new PhaseEventBuilder();
+ } else if (src.getEventType() == EventType.BCD_UPDATE) {
+ base = new BCDEventBuilder();
+ } else {
+ base = new ApiEventBuilder();
+ }
+ base.setTotalOrdering(src.getTotalOrdering())
+ .setUuid(src.getId())
+ .setSubscriptionId(src.getSubscriptionId())
+ .setCreatedDate(src.getCreatedDate())
+ .setUpdatedDate(src.getUpdatedDate())
+ .setEffectiveDate(src.getEffectiveDate())
+ .setActive(src.isActive());
SubscriptionBaseEvent result;
if (src.getEventType() == EventType.PHASE) {
@@ -184,6 +208,8 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
.setApiEventType(src.getUserType())
.setFromDisk(true);
result = builder.build();
+ } else if (src.getEventType() == EventType.BCD_UPDATE) {
+ result = (new BCDEventBuilder(base).setBillCycleDayLocal(src.getBillingCycleDayLocal())).build();
} else {
throw new SubscriptionBaseError(String.format("Can't figure out event %s", src.getEventType()));
}
@@ -202,6 +228,7 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
sb.append(", planName='").append(planName).append('\'');
sb.append(", phaseName='").append(phaseName).append('\'');
sb.append(", priceListName='").append(priceListName).append('\'');
+ sb.append(", billingCycleDayLocal=").append(billingCycleDayLocal);
sb.append(", isActive=").append(isActive);
sb.append('}');
return sb.toString();
@@ -248,7 +275,9 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
if (userType != that.userType) {
return false;
}
-
+ if (billingCycleDayLocal != that.billingCycleDayLocal) {
+ return false;
+ }
return true;
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
index 8003503..846be18 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
@@ -95,5 +95,6 @@ public interface SubscriptionDao extends EntityDao<SubscriptionBundleModelDao, S
public void updateBundleExternalKey(UUID bundleId, String externalKey, InternalCallContext context);
+ public void createBCDChangeEvent(DefaultSubscriptionBase subscription, SubscriptionBaseEvent bcdEvent, InternalCallContext context);
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java
index 33358f6..4ead49c 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java
@@ -18,7 +18,10 @@ package org.killbill.billing.subscription.engine.dao;
import java.util.Date;
import java.util.List;
+import java.util.UUID;
+import org.joda.time.DateTime;
+import org.killbill.billing.entity.EntityPersistenceException;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.BindBean;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
@@ -43,15 +46,13 @@ public interface SubscriptionEventSqlDao extends EntitySqlDao<SubscriptionEventM
@SqlQuery
public List<SubscriptionEventModelDao> getFutureActiveEventForSubscription(@Bind("subscriptionId") String subscriptionId,
- @Bind("now") Date now,
- @BindBean final InternalTenantContext context);
+ @Bind("now") Date now,
+ @BindBean final InternalTenantContext context);
@SqlQuery
public List<SubscriptionEventModelDao> getEventsForSubscription(@Bind("subscriptionId") String subscriptionId,
@BindBean final InternalTenantContext context);
-
@SqlQuery
public List<SubscriptionEventModelDao> getFutureActiveEventsForAccount(@Bind("now") Date now, @BindBean final InternalTenantContext context);
-
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEvent.java
new file mode 100644
index 0000000..011f54c
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEvent.java
@@ -0,0 +1,25 @@
+/*
+ * 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.subscription.events.bcd;
+
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+
+public interface BCDEvent extends SubscriptionBaseEvent {
+
+ public Integer getBillCycleDayLocal();
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventBuilder.java b/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventBuilder.java
new file mode 100644
index 0000000..7c0926d
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventBuilder.java
@@ -0,0 +1,55 @@
+/*
+ * 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.subscription.events.bcd;
+
+import org.killbill.billing.subscription.events.EventBaseBuilder;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+
+public class BCDEventBuilder extends EventBaseBuilder<BCDEventBuilder> {
+
+ private Integer billCycleDayLocal;
+
+ public BCDEventBuilder() {
+ super();
+ }
+
+
+ public BCDEventBuilder(final BCDEvent event) {
+ super(event);
+ this.billCycleDayLocal = event.getBillCycleDayLocal();
+ }
+
+ public BCDEventBuilder(final EventBaseBuilder<?> base) {
+ super(base);
+ }
+
+
+ @Override
+ public SubscriptionBaseEvent build() {
+ return new BCDEventData(this);
+ }
+
+ public Integer getBillCycleDayLocal() {
+ return billCycleDayLocal;
+ }
+
+ public BCDEventBuilder setBillCycleDayLocal(final Integer billCycleDayLocal) {
+ this.billCycleDayLocal = billCycleDayLocal;
+ return this;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventData.java b/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventData.java
new file mode 100644
index 0000000..c935d6c
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/bcd/BCDEventData.java
@@ -0,0 +1,65 @@
+/*
+ * 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.subscription.events.bcd;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.events.EventBase;
+
+public class BCDEventData extends EventBase implements BCDEvent {
+
+ private final Integer billCycleDayLocal;
+
+ public BCDEventData(final BCDEventBuilder builder) {
+ super(builder);
+ this.billCycleDayLocal = builder.getBillCycleDayLocal();
+ }
+
+ @Override
+ public EventType getType() {
+ return EventType.BCD_UPDATE;
+ }
+
+ @Override
+ public String toString() {
+ return "BCDEventData {" +
+ "uuid=" + getId() +
+ ", subscriptionId=" + getSubscriptionId() +
+ ", createdDate=" + getCreatedDate() +
+ ", updatedDate=" + getUpdatedDate() +
+ ", effectiveDate=" + getEffectiveDate() +
+ ", totalOrdering=" + getTotalOrdering() +
+ ", isActive=" + isActive() +
+ '}';
+ }
+
+ // Hack until we introduce a proper field for that
+ @Override
+ public Integer getBillCycleDayLocal() {
+ return billCycleDayLocal;
+ }
+
+ public static BCDEvent createBCDEvent(final DefaultSubscriptionBase subscription, final DateTime effectiveDate, final int billCycleDayLocal) {
+ return new BCDEventData(new BCDEventBuilder()
+ .setSubscriptionId(subscription.getId())
+ .setEffectiveDate(effectiveDate)
+ .setActive(true)
+ .setBillCycleDayLocal(billCycleDayLocal));
+ }
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java
index f710d44..36e7f24 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java
@@ -27,7 +27,8 @@ public interface SubscriptionBaseEvent extends Comparable<SubscriptionBaseEvent>
public enum EventType {
API_USER,
- PHASE
+ PHASE,
+ BCD_UPDATE
}
public EventType getType();
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql b/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql
index d10805e..9e5e0c6 100644
--- a/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql
@@ -4,13 +4,14 @@ DROP TABLE IF EXISTS subscription_events;
CREATE TABLE subscription_events (
record_id serial unique,
id varchar(36) NOT NULL,
- event_type varchar(9) NOT NULL,
+ event_type varchar(15) NOT NULL,
user_type varchar(25) DEFAULT NULL,
effective_date datetime NOT NULL,
subscription_id varchar(36) NOT NULL,
plan_name varchar(255) DEFAULT NULL,
phase_name varchar(255) DEFAULT NULL,
price_list_name varchar(64) DEFAULT NULL,
+ billing_cycle_day_local int DEFAULT NULL,
is_active boolean default true,
created_by varchar(50) NOT NULL,
created_date datetime NOT NULL,
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg
index a1f8af9..49cca79 100644
--- a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg
@@ -21,6 +21,7 @@ tableFields(prefix) ::= <<
, <prefix> plan_name
, <prefix> phase_name
, <prefix> price_list_name
+, <prefix> billing_cycle_day_local
, <prefix> is_active
, <prefix> created_by
, <prefix> created_date
@@ -36,6 +37,7 @@ tableValues() ::= <<
, :planName
, :phaseName
, :priceListName
+, :billingCycleDayLocal
, :isActive
, :createdBy
, :createdDate
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/migration/V20160915180903__cleanup_499.sql b/subscription/src/main/resources/org/killbill/billing/subscription/migration/V20160915180903__cleanup_499.sql
new file mode 100644
index 0000000..89dfedb
--- /dev/null
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/migration/V20160915180903__cleanup_499.sql
@@ -0,0 +1,4 @@
+alter table subscriptions drop column active_version;
+alter table subscriptions add column migrated bool NOT NULL default FALSE after charged_through_date;
+alter table subscription_events drop column requested_date;
+alter table subscription_events drop column current_version;
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/migration/V20160915180904__bcd_546.sql b/subscription/src/main/resources/org/killbill/billing/subscription/migration/V20160915180904__bcd_546.sql
new file mode 100644
index 0000000..4280909
--- /dev/null
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/migration/V20160915180904__bcd_546.sql
@@ -0,0 +1,2 @@
+alter table subscription_events change event_type event_type varchar(15) NOT NULL;
+alter table subscription_events add column billing_cycle_day_local int DEFAULT NULL after price_list_name;
\ No newline at end of file
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java b/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java
index cee8006..772055c 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java
@@ -37,7 +37,7 @@ public class TestEventJson extends SubscriptionTestSuiteNoDB {
public void testSubscriptionEvent() throws Exception {
final EffectiveSubscriptionInternalEvent e = new DefaultEffectiveSubscriptionEvent(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), new DateTime(),
- EntitlementState.ACTIVE, "pro", "TRIAL", "DEFAULT", EntitlementState.CANCELLED, null, null, null, 3L,
+ EntitlementState.ACTIVE, "pro", "TRIAL", "DEFAULT", null, EntitlementState.CANCELLED, null, null, null, null, 3L,
SubscriptionBaseTransitionType.CANCEL, 0, new DateTime(), 1L, 2L, null);
final String json = mapper.writeValueAsString(e);
@@ -46,4 +46,17 @@ public class TestEventJson extends SubscriptionTestSuiteNoDB {
final Object obj = mapper.readValue(json, claz);
Assert.assertTrue(obj.equals(e));
}
+
+ // Verify deserialization will work when we miss fields (previousBillCycleDayLocal, nextBillCycleDayLocal)
+ @Test(groups = "fast")
+ public void testSubscriptionEventWithNoBillCycleDayLocal() throws Exception {
+
+ final String json = "{\"eventId\":\"9e901bbc-bbcb-4f0a-8511-e58029bbea91\",\"subscriptionId\":\"c373056c-bb0c-4562-ab06-f595176aa4ae\",\"bundleId\":\"f61536b1-fc76-4337-b1e8-e38383894352\",\"effectiveTransitionTime\":\"2016-05-26T23:02:20.322Z\",\"previousState\":\"ACTIVE\",\"previousPlan\":\"pro\",\"previousPhase\":\"TRIAL\",\"previousPriceList\":\"DEFAULT\",\"nextState\":\"CANCELLED\",\"nextPlan\":null,\"nextPhase\":null,\"nextPriceList\":null,\"totalOrdering\":3,\"transitionType\":\"CANCEL\",\"remainingEventsForUserOperation\":0,\"startDate\":\"2016-05-26T23:02:20.322Z\",\"searchKey1\":1,\"searchKey2\":2,\"userToken\":null,\"requestedTransitionTime\":\"2016-05-26T23:02:20.322Z\"}";
+ final Class<?> claz = Class.forName(DefaultEffectiveSubscriptionEvent.class.getName());
+ final DefaultEffectiveSubscriptionEvent obj = (DefaultEffectiveSubscriptionEvent) mapper.readValue(json, claz);
+
+ Assert.assertEquals(obj.getId(), UUID.fromString("9e901bbc-bbcb-4f0a-8511-e58029bbea91"));
+ Assert.assertNull(obj.getPreviousBillCycleDayLocal());
+ Assert.assertNull(obj.getNextBillCycleDayLocal());
+ }
}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java
index 2a377af..c6704e1 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java
@@ -122,6 +122,11 @@ public class TestDefaultSubscriptionTransferApi extends SubscriptionTestSuiteNoD
}
@Override
+ public Integer getBillCycleDayLocal() {
+ return null;
+ }
+
+ @Override
public UUID getEventId() {
return UUID.randomUUID();
}
@@ -139,5 +144,4 @@ public class TestDefaultSubscriptionTransferApi extends SubscriptionTestSuiteNoD
}
};
}
-
}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
index 0dfe039..38500c3 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
@@ -501,4 +501,10 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
@Override
public void updateBundleExternalKey(final UUID bundleId, final String externalKey, final InternalCallContext context) {
}
+
+ @Override
+ public void createBCDChangeEvent(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent bcdEvent, final InternalCallContext context) {
+
+ }
+
}
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java
index 50923c6..7fb088e 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * 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
@@ -109,11 +109,17 @@ public class DefaultTenantInternalApi implements TenantInternalApi {
}
@Override
+ public String getPluginPaymentStateMachineConfig(final String pluginName, final InternalTenantContext tenantContext) {
+ final String pluginConfigKey = TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_ + pluginName;
+ final List<String> values = tenantDao.getTenantValueForKey(pluginConfigKey, tenantContext);
+ return getUniqueValue(values, "payment state machine for plugin " + pluginConfigKey, tenantContext);
+ }
+
+ @Override
public List<String> getTenantValuesForKey(final String key, final InternalTenantContext tenantContext) {
return tenantDao.getTenantValueForKey(key, tenantContext);
}
-
@Override
public Tenant getTenantByApiKey(final String key) throws TenantApiException {
final TenantModelDao tenant = tenantDao.getTenantByApiKey(key);
@@ -123,8 +129,6 @@ public class DefaultTenantInternalApi implements TenantInternalApi {
return new DefaultTenant(tenant);
}
-
-
private String getUniqueValue(final List<String> values, final String msg, final InternalTenantContext tenantContext) {
if (values.isEmpty()) {
return null;
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java b/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java
index 8bae33c..cc48190 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java
@@ -17,9 +17,8 @@
package org.killbill.billing.tenant.api;
-import java.util.HashMap;
+import java.util.Collection;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@@ -46,8 +45,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Predicate;
+import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
/**
* This class manages the callbacks that have been registered when per tenant objects have been inserted into the
@@ -65,7 +66,7 @@ public class TenantCacheInvalidation {
private static final Logger logger = LoggerFactory.getLogger(TenantCacheInvalidation.class);
- private final Map<TenantKey, CacheInvalidationCallback> cache;
+ private final Multimap<TenantKey, CacheInvalidationCallback> cache;
private final TenantBroadcastDao broadcastDao;
private final TenantConfig tenantConfig;
private final PersistentBus eventBus;
@@ -80,7 +81,7 @@ public class TenantCacheInvalidation {
@Named(DefaultTenantModule.NO_CACHING_TENANT) final TenantDao tenantDao,
final PersistentBus eventBus,
final TenantConfig tenantConfig) {
- this.cache = new HashMap<TenantKey, CacheInvalidationCallback>();
+ this.cache = HashMultimap.<TenantKey, CacheInvalidationCallback>create();
this.broadcastDao = broadcastDao;
this.tenantConfig = tenantConfig;
this.tenantDao = tenantDao;
@@ -122,12 +123,11 @@ public class TenantCacheInvalidation {
}
public void registerCallback(final TenantKey key, final CacheInvalidationCallback value) {
- if (!cache.containsKey(key)) {
- cache.put(key, value);
- }
+ cache.put(key, value);
+
}
- public CacheInvalidationCallback getCacheInvalidation(final TenantKey key) {
+ public Collection<CacheInvalidationCallback> getCacheInvalidations(final TenantKey key) {
return cache.get(key);
}
@@ -176,10 +176,12 @@ public class TenantCacheInvalidation {
try {
final TenantKeyAndCookie tenantKeyAndCookie = extractTenantKeyAndCookie(cur.getType());
if (tenantKeyAndCookie != null) {
- final CacheInvalidationCallback callback = parent.getCacheInvalidation(tenantKeyAndCookie.getTenantKey());
- if (callback != null) {
+ final Collection<CacheInvalidationCallback> callbacks = parent.getCacheInvalidations(tenantKeyAndCookie.getTenantKey());
+ if (!callbacks.isEmpty()) {
final InternalTenantContext tenantContext = new InternalTenantContext(cur.getTenantRecordId());
- callback.invalidateCache(tenantKeyAndCookie.getTenantKey(), tenantKeyAndCookie.getCookie(), tenantContext);
+ for (final CacheInvalidationCallback callback : callbacks) {
+ callback.invalidateCache(tenantKeyAndCookie.getTenantKey(), tenantKeyAndCookie.getCookie(), tenantContext);
+ }
final Long tenantKvsTargetRecordId = cur.getTargetRecordId();
final BusInternalEvent event;
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java b/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
index be914f5..5fec9ea 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
@@ -62,6 +62,7 @@ public class DefaultTenantUserApi implements TenantUserApi {
.add(TenantKey.INVOICE_TEMPLATE)
.add(TenantKey.INVOICE_TRANSLATION_)
.add(TenantKey.PLUGIN_CONFIG_)
+ .add(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_)
.add(TenantKey.PUSH_NOTIFICATION_CB).build();
private final TenantDao tenantDao;
diff --git a/usage/src/main/resources/org/killbill/billing/usage/migration/V20160915180905__tracking_id_502.sql b/usage/src/main/resources/org/killbill/billing/usage/migration/V20160915180905__tracking_id_502.sql
new file mode 100644
index 0000000..591631d
--- /dev/null
+++ b/usage/src/main/resources/org/killbill/billing/usage/migration/V20160915180905__tracking_id_502.sql
@@ -0,0 +1,3 @@
+alter table rolled_up_usage add column tracking_id varchar(128) NOT NULL after amount;
+alter table rolled_up_usage change subscription_id subscription_id varchar(36) not null;
+alter table rolled_up_usage change unit_type unit_type varchar(255) not null;
diff --git a/util/src/main/java/org/killbill/billing/util/bcd/BillCycleDayCalculator.java b/util/src/main/java/org/killbill/billing/util/bcd/BillCycleDayCalculator.java
new file mode 100644
index 0000000..a451153
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/bcd/BillCycleDayCalculator.java
@@ -0,0 +1,61 @@
+/*
+ * 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.util.bcd;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.killbill.billing.catalog.api.BillingAlignment;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.clock.ClockUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.VisibleForTesting;
+
+public class BillCycleDayCalculator {
+
+ private static final Logger log = LoggerFactory.getLogger(BillCycleDayCalculator.class);
+
+ public BillCycleDayCalculator() {
+ }
+
+ public static int calculateBcdForAlignment(final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final BillingAlignment alignment, final DateTimeZone accountTimeZone, final int accountBillCycleDayLocal) {
+ int result = 0;
+ switch (alignment) {
+ case ACCOUNT:
+ result = accountBillCycleDayLocal != 0 ? accountBillCycleDayLocal : calculateBcdFromSubscription(subscription, accountTimeZone);
+ break;
+ case BUNDLE:
+ result = calculateBcdFromSubscription(baseSubscription, accountTimeZone);
+ break;
+ case SUBSCRIPTION:
+ result = calculateBcdFromSubscription(subscription, accountTimeZone);
+ break;
+ }
+ return result;
+ }
+
+ @VisibleForTesting
+ static int calculateBcdFromSubscription(final SubscriptionBase subscription, final DateTimeZone accountTimeZone) {
+ final DateTime date = subscription.getDateOfFirstRecurringNonZeroCharge();
+ final int bcdLocal = ClockUtil.toDateTime(date, accountTimeZone).getDayOfMonth();
+ log.info("Calculated BCD: subscriptionId='{}', subscriptionStartDate='{}', accountTimeZone='{}', bcd='{}'",
+ subscription.getId(), date.toDateTimeISO(), accountTimeZone, bcdLocal);
+ return bcdLocal;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/Cachable.java b/util/src/main/java/org/killbill/billing/util/cache/Cachable.java
index f20c9a6..a70556c 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/Cachable.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/Cachable.java
@@ -32,6 +32,7 @@ public @interface Cachable {
String AUDIT_LOG_CACHE_NAME = "audit-log";
String AUDIT_LOG_VIA_HISTORY_CACHE_NAME = "audit-log-via-history";
String TENANT_CATALOG_CACHE_NAME = "tenant-catalog";
+ String TENANT_PAYMENT_STATE_MACHINE_CONFIG_CACHE_NAME = "tenant-payment-state-machine-config";
String TENANT_OVERDUE_CONFIG_CACHE_NAME = "tenant-overdue-config";
String TENANT_CONFIG_CACHE_NAME = "tenant-config";
String TENANT_KV_CACHE_NAME = "tenant-kv";
@@ -65,6 +66,9 @@ public @interface Cachable {
/* Tenant catalog cache */
TENANT_CATALOG(TENANT_CATALOG_CACHE_NAME, false),
+ /* Tenant payment state machine config cache */
+ TENANT_PAYMENT_STATE_MACHINE_CONFIG(TENANT_PAYMENT_STATE_MACHINE_CONFIG_CACHE_NAME, false),
+
/* Tenant overdue config cache */
TENANT_OVERDUE_CONFIG(TENANT_OVERDUE_CONFIG_CACHE_NAME, false),
diff --git a/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java b/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java
index bf2f6b1..bdf1302 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java
@@ -22,10 +22,14 @@ import java.util.Map;
import javax.inject.Inject;
import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
// Kill Bill generic cache dispatcher
public class CacheControllerDispatcher {
+ private static final Logger logger = LoggerFactory.getLogger(CacheControllerDispatcher.class);
+
public static final String CACHE_KEY_SEPARATOR = "::";
private final Map<CacheType, CacheController<Object, Object>> caches;
diff --git a/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java b/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java
index 7ce311c..305e6f2 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java
@@ -64,7 +64,8 @@ public class EhCacheCacheManagerProvider implements Provider<CacheManager> {
final TenantOverdueConfigCacheLoader tenantOverdueConfigCacheLoader,
final TenantKVCacheLoader tenantKVCacheLoader,
final TenantCacheLoader tenantCacheLoader,
- final OverriddenPlanCacheLoader overriddenPlanCacheLoader) {
+ final OverriddenPlanCacheLoader overriddenPlanCacheLoader,
+ final TenantStateMachineConfigCacheLoader tenantStateMachineConfigCacheLoader) {
this.metricRegistry = metricRegistry;
this.cacheConfig = cacheConfig;
cacheLoaders.add(accountCacheLoader);
@@ -81,6 +82,7 @@ public class EhCacheCacheManagerProvider implements Provider<CacheManager> {
cacheLoaders.add(tenantKVCacheLoader);
cacheLoaders.add(tenantCacheLoader);
cacheLoaders.add(overriddenPlanCacheLoader);
+ cacheLoaders.add(tenantStateMachineConfigCacheLoader);
}
@Override
@@ -100,6 +102,11 @@ public class EhCacheCacheManagerProvider implements Provider<CacheManager> {
final Ehcache cache = cacheManager.getEhcache(cacheLoader.getCacheType().getCacheName());
+ if (cache == null) {
+ logger.warn("Cache for cacheName='{}' not configured - check your ehcache.xml", cacheLoader.getCacheType().getCacheName());
+ continue;
+ }
+
// Make sure we start from a clean state - this is mainly useful for tests
for (final CacheLoader existingCacheLoader : cache.getRegisteredCacheLoaders()) {
cache.unregisterCacheLoader(existingCacheLoader);
diff --git a/util/src/main/java/org/killbill/billing/util/cache/TenantStateMachineConfigCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/TenantStateMachineConfigCacheLoader.java
new file mode 100644
index 0000000..c6d5489
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/TenantStateMachineConfigCacheLoader.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 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.util.cache;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.tenant.api.TenantInternalApi;
+import org.killbill.billing.tenant.api.TenantKV.TenantKey;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class TenantStateMachineConfigCacheLoader extends BaseCacheLoader {
+
+ private static final Pattern PATTERN = Pattern.compile(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString() + "(.*)");
+ private static final Logger log = LoggerFactory.getLogger(TenantStateMachineConfigCacheLoader.class);
+
+ private final TenantInternalApi tenantApi;
+
+ @Inject
+ public TenantStateMachineConfigCacheLoader(final TenantInternalApi tenantApi) {
+ super();
+ this.tenantApi = tenantApi;
+ }
+
+ @Override
+ public CacheType getCacheType() {
+ return CacheType.TENANT_PAYMENT_STATE_MACHINE_CONFIG;
+ }
+
+ @Override
+ public Object load(final Object key, final Object argument) {
+ checkCacheLoaderStatus();
+
+ if (!(key instanceof String)) {
+ throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
+ }
+ if (!(argument instanceof CacheLoaderArgument)) {
+ throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
+ }
+
+ final String[] parts = ((String) key).split(CacheControllerDispatcher.CACHE_KEY_SEPARATOR);
+ final String rawKey = parts[0];
+ final Matcher matcher = PATTERN.matcher(rawKey);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Unexpected key " + rawKey);
+ }
+ final String pluginName = matcher.group(1);
+ final String tenantRecordId = parts[1];
+
+ final CacheLoaderArgument cacheLoaderArgument = (CacheLoaderArgument) argument;
+ final LoaderCallback callback = (LoaderCallback) cacheLoaderArgument.getArgs()[0];
+
+ final InternalTenantContext internalTenantContext = new InternalTenantContext(Long.valueOf(tenantRecordId));
+ final String stateMachineConfigXML = tenantApi.getPluginPaymentStateMachineConfig(pluginName, internalTenantContext);
+ if (stateMachineConfigXML == null) {
+ return null;
+ }
+
+ try {
+ log.info("Loading config state machine cache for pluginName='{}', tenantRecordId='{}'", pluginName, internalTenantContext.getTenantRecordId());
+ return callback.loadStateMachineConfig(stateMachineConfigXML);
+ } catch (final PaymentApiException e) {
+ throw new IllegalStateException(String.format("Failed to de-serialize state machine config for tenantRecordId='%s'", internalTenantContext.getTenantRecordId()), e);
+ }
+ }
+
+ public interface LoaderCallback {
+
+ public Object loadStateMachineConfig(final String stateMachineConfigXML) throws PaymentApiException;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/definition/JaxrsConfig.java b/util/src/main/java/org/killbill/billing/util/config/definition/JaxrsConfig.java
index 38b6135..95fe22d 100644
--- a/util/src/main/java/org/killbill/billing/util/config/definition/JaxrsConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/definition/JaxrsConfig.java
@@ -34,4 +34,9 @@ public interface JaxrsConfig extends KillbillConfig {
@Description("Total timeout for all callables associated to a given api call (parallel mode)")
TimeSpan getJaxrsTimeout();
+ @Config("org.killbill.jaxrs.location.full.url")
+ @Default("true")
+ @Description("Type of return for the jaxrs response location URL")
+ boolean isJaxrsLocationFullUrl();
+
}
util/src/main/resources/ehcache.xml 18(+16 -2)
diff --git a/util/src/main/resources/ehcache.xml b/util/src/main/resources/ehcache.xml
index 6d9c27f..cd6b114 100644
--- a/util/src/main/resources/ehcache.xml
+++ b/util/src/main/resources/ehcache.xml
@@ -1,9 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
- ~ Copyright 2010-2013 Ning, Inc.
+ ~ Copyright 2010-2014 Ning, Inc.
+ ~ Copyright 2014-2016 Groupon, Inc
+ ~ Copyright 2014-2016 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:
~
@@ -219,6 +221,18 @@
properties=""/>
</cache>
+ <cache name="tenant-payment-state-machine-config"
+ maxElementsInMemory="100"
+ maxElementsOnDisk="0"
+ overflowToDisk="false"
+ diskPersistent="false"
+ memoryStoreEvictionPolicy="LFU"
+ statistics="true"
+ >
+ <cacheEventListenerFactory
+ class="org.killbill.billing.util.cache.ExpirationListenerFactory"
+ properties=""/>
+ </cache>
</ehcache>
diff --git a/util/src/test/java/org/killbill/billing/api/TestApiListener.java b/util/src/test/java/org/killbill/billing/api/TestApiListener.java
index 61c1252..be41d8d 100644
--- a/util/src/test/java/org/killbill/billing/api/TestApiListener.java
+++ b/util/src/test/java/org/killbill/billing/api/TestApiListener.java
@@ -124,6 +124,7 @@ public class TestApiListener {
TAG,
TAG_DEFINITION,
CUSTOM_FIELD,
+ BCD_CHANGE
}
@Subscribe
@@ -168,6 +169,10 @@ public class TestApiListener {
assertEqualsNicely(NextEvent.PHASE);
notifyIfStackEmpty();
break;
+ case BCD_CHANGE:
+ assertEqualsNicely(NextEvent.BCD_CHANGE);
+ notifyIfStackEmpty();
+ break;
default:
throw new RuntimeException("Unexpected event type " + eventEffective.getRequestedTransitionTime());
}
diff --git a/util/src/test/java/org/killbill/billing/GuicyKillbillTestWithEmbeddedDBModule.java b/util/src/test/java/org/killbill/billing/GuicyKillbillTestWithEmbeddedDBModule.java
index 67915ce..edeef1b 100644
--- a/util/src/test/java/org/killbill/billing/GuicyKillbillTestWithEmbeddedDBModule.java
+++ b/util/src/test/java/org/killbill/billing/GuicyKillbillTestWithEmbeddedDBModule.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * 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
@@ -19,6 +19,7 @@
package org.killbill.billing;
import org.killbill.billing.platform.api.KillbillConfigSource;
+import org.killbill.billing.platform.test.PlatformDBTestingHelper;
import org.killbill.billing.platform.test.config.TestKillbillConfigSource;
import org.killbill.billing.platform.test.glue.TestPlatformModuleWithEmbeddedDB;
@@ -48,12 +49,11 @@ public class GuicyKillbillTestWithEmbeddedDBModule extends GuicyKillbillTestModu
super(configSource, withOSGI, (TestKillbillConfigSource) configSource);
}
- protected void configureEmbeddedDB() {
- final DBTestingHelper dbTestingHelper = DBTestingHelper.get();
- configureEmbeddedDB(dbTestingHelper);
+ @Override
+ protected PlatformDBTestingHelper getPlatformDBTestingHelper() {
+ return DBTestingHelper.get();
}
- protected void configureKillbillNodesApi() {
- }
+ protected void configureKillbillNodesApi() {}
}
}
diff --git a/util/src/test/java/org/killbill/billing/mock/MockEffectiveSubscriptionEvent.java b/util/src/test/java/org/killbill/billing/mock/MockEffectiveSubscriptionEvent.java
index 793f2f0..60f76fa 100644
--- a/util/src/test/java/org/killbill/billing/mock/MockEffectiveSubscriptionEvent.java
+++ b/util/src/test/java/org/killbill/billing/mock/MockEffectiveSubscriptionEvent.java
@@ -39,10 +39,12 @@ public class MockEffectiveSubscriptionEvent extends BusEventBase implements Effe
private final DateTime effectiveTransitionTime;
private final EntitlementState previousState;
private final String previousPriceList;
+ private final Integer previousBillCycleDayLocal;
private final String previousPlan;
private final String previousPhase;
private final EntitlementState nextState;
private final String nextPriceList;
+ private final Integer nextBillCycleDayLocal;
private final String nextPlan;
private final String nextPhase;
private final Integer remainingEventsForUserOperation;
@@ -61,10 +63,12 @@ public class MockEffectiveSubscriptionEvent extends BusEventBase implements Effe
@JsonProperty("previousPlan") final String previousPlan,
@JsonProperty("previousPhase") final String previousPhase,
@JsonProperty("previousPriceList") final String previousPriceList,
+ @JsonProperty("previousBillCycleDayLocal") final Integer previousBillCycleDayLocal,
@JsonProperty("nextState") final EntitlementState nextState,
@JsonProperty("nextPlan") final String nextPlan,
@JsonProperty("nextPhase") final String nextPhase,
@JsonProperty("nextPriceList") final String nextPriceList,
+ @JsonProperty("nextBillCycleDayLocal") final Integer nextBillCycleDayLocal,
@JsonProperty("totalOrdering") final Long totalOrdering,
@JsonProperty("transitionType") final SubscriptionBaseTransitionType transitionType,
@JsonProperty("remainingEventsForUserOperation") final Integer remainingEventsForUserOperation,
@@ -80,11 +84,13 @@ public class MockEffectiveSubscriptionEvent extends BusEventBase implements Effe
this.effectiveTransitionTime = effectiveTransitionTime;
this.previousState = previousState;
this.previousPriceList = previousPriceList;
+ this.previousBillCycleDayLocal = previousBillCycleDayLocal;
this.previousPlan = previousPlan;
this.previousPhase = previousPhase;
this.nextState = nextState;
this.nextPlan = nextPlan;
this.nextPriceList = nextPriceList;
+ this.nextBillCycleDayLocal = nextBillCycleDayLocal;
this.nextPhase = nextPhase;
this.totalOrdering = totalOrdering;
this.userToken = userToken;
@@ -132,6 +138,11 @@ public class MockEffectiveSubscriptionEvent extends BusEventBase implements Effe
}
@Override
+ public Integer getPreviousBillCycleDayLocal() {
+ return previousBillCycleDayLocal;
+ }
+
+ @Override
public String getNextPlan() {
return nextPlan;
}
@@ -158,6 +169,11 @@ public class MockEffectiveSubscriptionEvent extends BusEventBase implements Effe
}
@Override
+ public Integer getNextBillCycleDayLocal() {
+ return nextBillCycleDayLocal;
+ }
+
+ @Override
public Integer getRemainingEventsForUserOperation() {
return remainingEventsForUserOperation;
}
diff --git a/util/src/test/java/org/killbill/billing/mock/MockSubscription.java b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
index f852a1b..6529062 100644
--- a/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
+++ b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
@@ -20,7 +20,6 @@ import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.mockito.Mockito;
@@ -37,9 +36,6 @@ import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
import org.killbill.billing.util.callcontext.CallContext;
-import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
-
-import com.google.common.collect.ImmutableList;
public class MockSubscription implements SubscriptionBase {
@@ -49,30 +45,20 @@ public class MockSubscription implements SubscriptionBase {
private Plan plan;
private final PlanPhase phase;
private final DateTime startDate;
- private final List<EffectiveSubscriptionInternalEvent> transitions;
+ private final DateTime firstRecurringNonZeroChargeDate;
+ private SubscriptionBase sub;
- public MockSubscription(final UUID id, final UUID bundleId, final Plan plan, final DateTime startDate, final List<EffectiveSubscriptionInternalEvent> transitions) {
+ public MockSubscription(final UUID id, final UUID bundleId, final Plan plan, final DateTime startDate, final DateTime firstRecurringNonZeroChargeDate) {
this.id = id;
this.bundleId = bundleId;
this.state = EntitlementState.ACTIVE;
this.plan = plan;
this.phase = null;
this.startDate = startDate;
- this.transitions = transitions;
- }
-
- public MockSubscription(final EntitlementState state, final Plan plan, final PlanPhase phase) {
- this.id = UUID.randomUUID();
- this.bundleId = UUID.randomUUID();
- this.state = state;
- this.plan = plan;
- this.phase = phase;
- this.startDate = new DateTime(DateTimeZone.UTC);
- this.transitions = ImmutableList.<EffectiveSubscriptionInternalEvent>of();
+ this.firstRecurringNonZeroChargeDate = firstRecurringNonZeroChargeDate;
+ this.sub = Mockito.mock(SubscriptionBase.class);
}
- SubscriptionBase sub = Mockito.mock(SubscriptionBase.class);
-
@Override
public boolean cancel(final CallContext context) throws SubscriptionBaseApiException {
return sub.cancel(context);
@@ -167,6 +153,11 @@ public class MockSubscription implements SubscriptionBase {
}
@Override
+ public DateTime getDateOfFirstRecurringNonZeroCharge() {
+ return firstRecurringNonZeroChargeDate;
+ }
+
+ @Override
public boolean isMigrated() {
return false;
}
@@ -177,6 +168,11 @@ public class MockSubscription implements SubscriptionBase {
}
@Override
+ public Integer getBillCycleDayLocal() {
+ return null;
+ }
+
+ @Override
public DateTime getFutureEndDate() {
return sub.getFutureEndDate();
}
@@ -222,13 +218,11 @@ public class MockSubscription implements SubscriptionBase {
@Override
public SubscriptionBaseTransition getPendingTransition() {
- // TODO Auto-generated method stub
return null;
}
@Override
public SubscriptionBaseTransition getPreviousTransition() {
- // TODO Auto-generated method stub
return null;
}