killbill-uncached
Changes
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestBillingAlignment.java 57(+39 -18)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java 11(+8 -3)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java 77(+36 -41)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueWithSubscriptionCancellation.java 5(+3 -2)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueWithTags.java 57(+50 -7)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java 375(+349 -26)
entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java 12(+0 -12)
entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java 8(+2 -6)
entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java 13(+4 -9)
entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestProxyBlockingStateDao.java 2(+1 -1)
invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestProRation.java 26(+18 -8)
overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java 3(+2 -1)
payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java 28(+17 -11)
payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java 24(+21 -3)
payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackOperation.java 27(+1 -26)
payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java 12(+9 -3)
payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java 46(+33 -13)
payment/src/test/java/org/killbill/billing/payment/glue/MockEntitlementModuleForPayment.java 2(+1 -1)
payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java 29(+25 -4)
pom.xml 2(+1 -1)
Details
diff --git a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
index b50879e..6c44797 100644
--- a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
@@ -43,8 +43,6 @@ public interface InvoiceInternalApi {
public InvoicePayment getInvoicePaymentForAttempt(UUID paymentId, InternalTenantContext context) throws InvoiceApiException;
- public InvoicePayment getInvoicePaymentForRefund(UUID paymentId, InternalTenantContext context) throws InvoiceApiException;
-
public InvoicePayment getInvoicePaymentForChargeback(UUID paymentId, InternalTenantContext context) throws InvoiceApiException;
public Invoice getInvoiceForPaymentId(UUID paymentId, InternalTenantContext context) throws InvoiceApiException;
diff --git a/api/src/main/java/org/killbill/billing/junction/DefaultBlockingState.java b/api/src/main/java/org/killbill/billing/junction/DefaultBlockingState.java
index b548665..07537b0 100644
--- a/api/src/main/java/org/killbill/billing/junction/DefaultBlockingState.java
+++ b/api/src/main/java/org/killbill/billing/junction/DefaultBlockingState.java
@@ -29,9 +29,6 @@ import org.killbill.clock.Clock;
public class DefaultBlockingState extends EntityBase implements BlockingState {
- public static final String CLEAR_STATE_NAME = "__KILLBILL__CLEAR__OVERDUE_STATE__";
-
- private static BlockingState clearState = null;
private final UUID blockedId;
private final String stateName;
@@ -43,13 +40,6 @@ public class DefaultBlockingState extends EntityBase implements BlockingState {
private final BlockingStateType type;
private final Long totalOrdering;
- public static BlockingState getClearState(final BlockingStateType type, final String serviceName, final Clock clock) {
- if (clearState == null) {
- clearState = new DefaultBlockingState(null, type, CLEAR_STATE_NAME, serviceName, false, false, false, clock.getUTCNow());
- }
- return clearState;
- }
-
// Used by the DAO
public DefaultBlockingState(final UUID id,
final UUID blockedId,
diff --git a/api/src/main/java/org/killbill/billing/util/UUIDs.java b/api/src/main/java/org/killbill/billing/util/UUIDs.java
index 9a89bdd..c1fc204 100644
--- a/api/src/main/java/org/killbill/billing/util/UUIDs.java
+++ b/api/src/main/java/org/killbill/billing/util/UUIDs.java
@@ -1,5 +1,6 @@
/*
- * Copyright 2015 The Billing Project, LLC
+ * Copyright 2015-2016 Groupon, Inc
+ * Copyright 2015-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
@@ -30,6 +31,14 @@ public abstract class UUIDs {
public static UUID randomUUID() { return rndUUIDv4(); }
+ public static void setRandom(final Random random) {
+ threadRandom.set(random);
+ }
+
+ public static Random getRandom() {
+ return threadRandom.get();
+ }
+
private static UUID rndUUIDv4() {
// ~ return UUID.randomUUID() :
final Random random = threadRandom.get();
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestBillingAlignment.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestBillingAlignment.java
index 086fab5..9ee5303 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestBillingAlignment.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestBillingAlignment.java
@@ -21,8 +21,6 @@ package org.killbill.billing.beatrix.integration.overdue;
import java.math.BigDecimal;
import org.joda.time.LocalDate;
-import org.testng.annotations.Test;
-
import org.killbill.billing.account.api.Account;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.integration.TestIntegrationBase;
@@ -31,22 +29,18 @@ import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
+import org.testng.Assert;
+import org.testng.annotations.Test;
import static org.testng.Assert.assertNotNull;
public class TestBillingAlignment extends TestIntegrationBase {
- // TODO test fails as it should not create a proration when the chnage to annual occurs. Instaed we should restart from the data of the chnage
- // since we have as a catalog rule:
- // <billingAlignmentCase>
- // <billingPeriod>ANNUAL</billingPeriod>
- // <alignment>SUBSCRIPTION</alignment>
- // </billingAlignmentCase>
- //
- @Test(groups = "slow", enabled = false)
- public void testTransitonAccountBAToSubscriptionBA() throws Exception {
-
- final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+ @Test(groups = "slow")
+ public void testTransitionAccountBAToSubscriptionBA() throws Exception {
+ // Set the BCD to the 25th
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(25));
// We take april as it has 30 days (easier to play with BCD)
// Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
@@ -54,17 +48,44 @@ public class TestBillingAlignment extends TestIntegrationBase {
//
// CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
- // (Start with monthly that has a 'Account' billing alignment
+ // (Start with monthly that has an 'Account' billing alignment)
//
final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
assertNotNull(bpEntitlement);
+
invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ invoiceChecker.checkChargedThroughDate(bpEntitlement.getId(), new LocalDate(2012, 4, 1), callContext);
- // GET OUT TRIAL
+ // GET OUT TRIAL (moving clock to 2012-05-04)
addDaysAndCheckForCompletion(33, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
- //
- // Change plan to annual that has been configured to have a 'SubscriptionBase' billing alignment
- changeEntitlementAndCheckForCompletion(bpEntitlement, "Shotgun", BillingPeriod.ANNUAL, null, NextEvent.CHANGE, NextEvent.INVOICE);
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 25), InvoiceItemType.RECURRING, new BigDecimal("199.96")));
+ invoiceChecker.checkChargedThroughDate(bpEntitlement.getId(), new LocalDate(2012, 5, 25), callContext);
+
+ // Change plan to annual that has been configured to have a 'Subscription' billing alignment
+ final DefaultEntitlement changedBpEntitlement = changeEntitlementAndCheckForCompletion(bpEntitlement, "Shotgun", BillingPeriod.ANNUAL, null, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ invoiceChecker.checkInvoice(account.getId(),
+ 3,
+ callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 4), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2380.22")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 4), new LocalDate(2012, 5, 25), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-174.97")));
+ invoiceChecker.checkChargedThroughDate(bpEntitlement.getId(), new LocalDate(2013, 5, 1), callContext);
+
+ Assert.assertEquals(changedBpEntitlement.getSubscriptionBase().getAllTransitions().size(), 3);
+
+ final SubscriptionBaseTransition trial = changedBpEntitlement.getSubscriptionBase().getAllTransitions().get(0);
+ Assert.assertEquals(trial.getEffectiveTransitionTime().toLocalDate().compareTo(new LocalDate(2012, 4, 1)), 0);
+ Assert.assertEquals(trial.getNextPhase().getName(), "shotgun-monthly-trial");
+
+ final SubscriptionBaseTransition smEvergreen = changedBpEntitlement.getSubscriptionBase().getAllTransitions().get(1);
+ Assert.assertEquals(smEvergreen.getEffectiveTransitionTime().toLocalDate().compareTo(new LocalDate(2012, 5, 1)), 0);
+ Assert.assertEquals(smEvergreen.getNextPhase().getName(), "shotgun-monthly-evergreen");
+
+ final SubscriptionBaseTransition saEvergreen = changedBpEntitlement.getSubscriptionBase().getAllTransitions().get(2);
+ // Verify the IMMEDIATE policy
+ Assert.assertEquals(saEvergreen.getEffectiveTransitionTime().toLocalDate().compareTo(new LocalDate(2012, 5, 4)), 0);
+ // Verify the START_OF_SUBSCRIPTION alignment (both plans have the same 30 days trial)
+ Assert.assertEquals(saEvergreen.getNextPhase().getName(), "shotgun-annual-evergreen");
}
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
index ccc25d5..5f5b4e2 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
@@ -27,11 +27,12 @@ import org.killbill.billing.account.api.Account;
import org.killbill.billing.beatrix.integration.BeatrixIntegrationModule;
import org.killbill.billing.beatrix.integration.TestIntegrationBase;
import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.SubscriptionBundle;
import org.killbill.billing.overdue.OverdueService;
-import org.killbill.billing.overdue.api.DefaultOverdueInternalApi;
import org.killbill.billing.overdue.config.DefaultOverdueConfig;
+import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.killbill.billing.payment.api.PaymentMethodPlugin;
import org.killbill.billing.payment.api.TestPaymentMethodPluginBase;
import org.killbill.xmlloader.XMLLoader;
@@ -81,11 +82,15 @@ public abstract class TestOverdueBase extends TestIntegrationBase {
await().atMost(10, SECONDS).until(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
- return expected.equals(blockingApi.getBlockingStateForService(account.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext).getStateName());
+ final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(account.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
+ final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
+ return expected.equals(stateName);
}
});
} catch (final Exception e) {
- Assert.assertEquals(blockingApi.getBlockingStateForService(account.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext).getStateName(), expected, "Got exception: " + e.toString());
+ final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(account.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
+ final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
+ Assert.assertEquals(stateName, expected, "Got exception: " + e.toString());
}
}
}
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 a974451..b97040b 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
@@ -45,7 +45,7 @@ import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoicePayment;
import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
-import org.killbill.billing.junction.DefaultBlockingState;
+import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PluginProperty;
import org.testng.annotations.Test;
@@ -136,15 +136,15 @@ public class TestOverdueIntegration extends TestOverdueBase {
// 2012-06-08 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-16 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-24 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-30 => P1
addDaysAndCheckForCompletion(6, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
@@ -223,15 +223,15 @@ public class TestOverdueIntegration extends TestOverdueBase {
// 2012-06-08 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-16 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-24 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-30 => P1
addDaysAndCheckForCompletion(6, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
@@ -317,15 +317,15 @@ public class TestOverdueIntegration extends TestOverdueBase {
// 2012-06-08 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-16 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-24 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-30
addDaysAndCheckForCompletion(6, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
@@ -417,15 +417,15 @@ public class TestOverdueIntegration extends TestOverdueBase {
// 2012-06-08 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-16 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-24 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-30
addDaysAndCheckForCompletion(6, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
@@ -506,15 +506,15 @@ public class TestOverdueIntegration extends TestOverdueBase {
// 2012-06-08 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-16 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-24 => Retry P0
addDaysAndCheckForCompletion(8, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-30 => OD1
addDaysAndCheckForCompletion(6, NextEvent.BLOCK);
@@ -585,13 +585,13 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 30), callContext);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-15 => DAY 45 - 15 days after invoice
addDaysAndCheckForCompletion(15);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-07-05 => DAY 65 - 35 days after invoice
// Single PAYMENT_ERROR here here triggered by the invoice
@@ -686,7 +686,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
// Should still be in clear state - the invoice for the bundle has been paid, but not the invoice with the external charge
// We refresh overdue just to be safe, see below
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-06 => Past 30 days since the external charge
addDaysAndCheckForCompletion(6, NextEvent.BLOCK);
@@ -697,7 +697,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
final Invoice externalChargeInvoice = invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).iterator().next();
createExternalPaymentAndCheckForCompletion(account, externalChargeInvoice, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
// We should be clear now
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
}
@Test(groups = "slow", description = "Test overdue after refund with no adjustment")
@@ -719,13 +719,13 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 30), callContext);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-15 => DAY 45 - 15 days after invoice
addDaysAndCheckForCompletion(15);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-07-05 => DAY 65 - 35 days after invoice
addDaysAndCheckForCompletion(20, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
@@ -734,7 +734,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 7, 31), callContext);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// Now, refund the second (first non-zero dollar) invoice
final Payment payment = paymentApi.getPayment(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).get(1).getPayments().get(0).getPaymentId(), false, PLUGIN_PROPERTIES, callContext);
@@ -763,13 +763,13 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 30), callContext);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-15 => DAY 45 - 15 days after invoice
addDaysAndCheckForCompletion(15);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-07-05 => DAY 65 - 35 days after invoice
addDaysAndCheckForCompletion(20, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
@@ -778,7 +778,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 7, 31), callContext);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// Now, create a chargeback for the second (first non-zero dollar) invoice
final InvoicePayment invoicePayment = invoicePaymentApi.getInvoicePayments(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).get(1).getPayments().get(0).getPaymentId(), callContext).get(0);
@@ -809,13 +809,13 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 30), callContext);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-06-15 => DAY 45 - 15 days after invoice
addDaysAndCheckForCompletion(15, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-07-05 => DAY 65 - 35 days after invoice
addDaysAndCheckForCompletion(20, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
@@ -834,7 +834,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
final Invoice firstNonZeroInvoice = invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).iterator().next();
createExternalPaymentAndCheckForCompletion(account, firstNonZeroInvoice, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
// We should be clear now
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
}
@Test(groups = "slow", description = "Test overdue clear after item adjustment")
@@ -857,13 +857,13 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 30), callContext);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// DAY 45 - 15 days after invoice
addDaysAndCheckForCompletion(15, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// DAY 65 - 35 days after invoice
addDaysAndCheckForCompletion(20, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
@@ -881,7 +881,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
final Invoice firstNonZeroInvoice = invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).iterator().next();
fullyAdjustInvoiceItemAndCheckForCompletion(account, firstNonZeroInvoice, 1, NextEvent.BLOCK, NextEvent.INVOICE_ADJUSTMENT);
// We should be clear now
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
invoiceChecker.checkInvoice(account.getId(), 2,
callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
@@ -892,13 +892,13 @@ public class TestOverdueIntegration extends TestOverdueBase {
addDaysAndCheckForCompletion(5);
// We should still be clear
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// DAY 80 - 20 days after second invoice
addDaysAndCheckForCompletion(10, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
// We should still be clear
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// DAY 95 - 35 days after second invoice
addDaysAndCheckForCompletion(15, NextEvent.BLOCK, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
@@ -920,12 +920,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
}
// We should be cleared again
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
- }
-
- @Test(groups = "slow", enabled = false)
- public void testOverdueStateAndWRITTEN_OFFTag() throws Exception {
- // TODO add/remove tag to invoice
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
}
private void allowPaymentsAndResetOverdueToClearByPayingAllUnpaidInvoices(final boolean extraPayment) {
@@ -955,7 +950,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
}
}
}
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
}
private List<Invoice> getUnpaidInvoicesOrderFromRecent() {
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueWithSubscriptionCancellation.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueWithSubscriptionCancellation.java
index 026d500..9575752 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueWithSubscriptionCancellation.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueWithSubscriptionCancellation.java
@@ -31,6 +31,7 @@ import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.api.SubscriptionBundle;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.junction.DefaultBlockingState;
+import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.testng.annotations.Test;
@@ -92,7 +93,7 @@ public class TestOverdueWithSubscriptionCancellation extends TestOverdueBase {
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// DAY 36 -- RIGHT AFTER OD1 (two block events, for the cancellation and the OD1 state)
@@ -148,7 +149,7 @@ public class TestOverdueWithSubscriptionCancellation extends TestOverdueBase {
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 30), callContext);
// Should still be in clear state
- checkODState(DefaultBlockingState.CLEAR_STATE_NAME);
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// DAY 36 (2012-06-06)-- RIGHT AFTER OD1 (two block events, for the cancellation and the OD1 state)
// One BLOCK event is for the overdue state transition
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 a52037e..fef7033 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
@@ -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
@@ -20,6 +20,7 @@ package org.killbill.billing.beatrix.integration;
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -98,7 +99,8 @@ import org.killbill.billing.util.api.RecordIdApi;
import org.killbill.billing.util.api.TagApiException;
import org.killbill.billing.util.api.TagDefinitionApiException;
import org.killbill.billing.util.api.TagUserApi;
-import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TestCallContext;
import org.killbill.billing.util.nodes.KillbillNodesApi;
import org.killbill.billing.util.tag.ControlTagType;
import org.killbill.billing.util.tag.Tag;
@@ -413,12 +415,11 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
@Override
public Payment apply(@Nullable final Void input) {
try {
-
final List<PluginProperty> properties = new ArrayList<PluginProperty>();
final PluginProperty prop1 = new PluginProperty(InvoicePaymentControlPluginApi.PROP_IPCD_INVOICE_ID, invoice.getId().toString(), false);
properties.add(prop1);
return paymentApi.createPurchaseWithPaymentControl(account, account.getPaymentMethodId(), null, amount, currency, UUID.randomUUID().toString(),
- UUID.randomUUID().toString(), properties, PAYMENT_OPTIONS, callContext);
+ UUID.randomUUID().toString(), properties, PAYMENT_OPTIONS, refreshedCallContext());
} catch (final PaymentApiException e) {
fail(e.toString());
return null;
@@ -437,7 +438,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
properties.add(prop1);
return paymentApi.createPurchaseWithPaymentControl(account, account.getPaymentMethodId(), null, invoice.getBalance(), invoice.getCurrency(), UUID.randomUUID().toString(),
- UUID.randomUUID().toString(), properties, PAYMENT_OPTIONS, callContext);
+ UUID.randomUUID().toString(), properties, PAYMENT_OPTIONS, refreshedCallContext());
} catch (final PaymentApiException e) {
fail(e.toString());
return null;
@@ -457,7 +458,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
properties.add(prop1);
return paymentApi.createPurchaseWithPaymentControl(account, account.getPaymentMethodId(), null, invoice.getBalance(), invoice.getCurrency(), UUID.randomUUID().toString(),
- UUID.randomUUID().toString(), properties, EXTERNAL_PAYMENT_OPTIONS, callContext);
+ UUID.randomUUID().toString(), properties, EXTERNAL_PAYMENT_OPTIONS, refreshedCallContext());
} catch (final PaymentApiException e) {
fail(e.toString());
return null;
@@ -467,12 +468,16 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
}
protected Payment refundPaymentAndCheckForCompletion(final Account account, final Payment payment, final NextEvent... events) {
+ return refundPaymentAndCheckForCompletion(account, payment, payment.getPurchasedAmount(), payment.getCurrency(), events);
+ }
+
+ protected Payment refundPaymentAndCheckForCompletion(final Account account, final Payment payment, final BigDecimal amount, final Currency currency, final NextEvent... events) {
return doCallAndCheckForCompletion(new Function<Void, Payment>() {
@Override
public Payment apply(@Nullable final Void input) {
try {
- return paymentApi.createRefundWithPaymentControl(account, payment.getId(), payment.getPurchasedAmount(), payment.getCurrency(), UUID.randomUUID().toString(),
- PLUGIN_PROPERTIES, PAYMENT_OPTIONS, callContext);
+ return paymentApi.createRefundWithPaymentControl(account, payment.getId(), amount, currency, UUID.randomUUID().toString(),
+ PLUGIN_PROPERTIES, PAYMENT_OPTIONS, refreshedCallContext());
} catch (final PaymentApiException e) {
fail(e.toString());
return null;
@@ -491,7 +496,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
properties.add(prop1);
try {
return paymentApi.createRefundWithPaymentControl(account, payment.getId(), payment.getPurchasedAmount(), payment.getCurrency(), UUID.randomUUID().toString(),
- properties, PAYMENT_OPTIONS, callContext);
+ properties, PAYMENT_OPTIONS, refreshedCallContext());
} catch (final PaymentApiException e) {
fail(e.toString());
return null;
@@ -501,19 +506,22 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
}
protected Payment refundPaymentWithInvoiceItemAdjAndCheckForCompletion(final Account account, final Payment payment, final Map<UUID, BigDecimal> iias, final NextEvent... events) {
+ return refundPaymentWithInvoiceItemAdjAndCheckForCompletion(account, payment, payment.getPurchasedAmount(), payment.getCurrency(), iias, events);
+ }
+
+ protected Payment refundPaymentWithInvoiceItemAdjAndCheckForCompletion(final Account account, final Payment payment, final BigDecimal amount, final Currency currency, final Map<UUID, BigDecimal> iias, final NextEvent... events) {
return doCallAndCheckForCompletion(new Function<Void, Payment>() {
@Override
public Payment apply(@Nullable final Void input) {
-
- final List<PluginProperty> properties = new ArrayList<PluginProperty>();
+ final Collection<PluginProperty> properties = new ArrayList<PluginProperty>();
final PluginProperty prop1 = new PluginProperty(InvoicePaymentControlPluginApi.PROP_IPCD_REFUND_WITH_ADJUSTMENTS, "true", false);
properties.add(prop1);
final PluginProperty prop2 = new PluginProperty(InvoicePaymentControlPluginApi.PROP_IPCD_REFUND_IDS_WITH_AMOUNT_KEY, iias, false);
properties.add(prop2);
try {
- return paymentApi.createRefundWithPaymentControl(account, payment.getId(), payment.getPurchasedAmount(), payment.getCurrency(), UUID.randomUUID().toString(),
- properties, PAYMENT_OPTIONS, callContext);
+ return paymentApi.createRefundWithPaymentControl(account, payment.getId(), amount, currency, UUID.randomUUID().toString(),
+ properties, PAYMENT_OPTIONS, refreshedCallContext());
} catch (final PaymentApiException e) {
fail(e.toString());
return null;
@@ -522,18 +530,21 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
}, events);
}
- protected void createChargeBackAndCheckForCompletion(final Account account, final Payment payment, final NextEvent... events) {
- doCallAndCheckForCompletion(new Function<Void, Void>() {
+ protected Payment createChargeBackAndCheckForCompletion(final Account account, final Payment payment, final NextEvent... events) {
+ return createChargeBackAndCheckForCompletion(account, payment, payment.getPurchasedAmount(), payment.getCurrency(), events);
+ }
+
+ protected Payment createChargeBackAndCheckForCompletion(final Account account, final Payment payment, final BigDecimal amount, final Currency currency, final NextEvent... events) {
+ return doCallAndCheckForCompletion(new Function<Void, Payment>() {
@Override
- public Void apply(@Nullable final Void input) {
+ public Payment apply(@Nullable final Void input) {
try {
- paymentApi.createChargebackWithPaymentControl(account, payment.getId(), payment.getPurchasedAmount(), payment.getCurrency(), UUID.randomUUID().toString(),
- PAYMENT_OPTIONS, callContext);
- } catch (PaymentApiException e) {
+ return paymentApi.createChargebackWithPaymentControl(account, payment.getId(), amount, currency, UUID.randomUUID().toString(),
+ PAYMENT_OPTIONS, refreshedCallContext());
+ } catch (final PaymentApiException e) {
fail(e.toString());
return null;
}
- return null;
}
}, events);
}
@@ -700,6 +711,11 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
assertListenerStatus();
}
+ // Update the context dates (matters for payments ordering for instance)
+ protected CallContext refreshedCallContext() {
+ return new TestCallContext(callContext, clock.getUTCNow());
+ }
+
private <T> T doCallAndCheckForCompletion(final Function<Void, T> f, final NextEvent... events) {
final Joiner joiner = Joiner.on(", ");
log.debug(" ************ STARTING BUS HANDLER CHECK : {} ********************", joiner.join(events));
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 c18cd69..e79eb43 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
@@ -18,13 +18,19 @@
package org.killbill.billing.beatrix.integration;
import java.math.BigDecimal;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountData;
import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import org.killbill.billing.beatrix.util.PaymentChecker.ExpectedPaymentCheck;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.ProductCategory;
@@ -32,9 +38,12 @@ import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.api.InvoicePaymentType;
import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.api.TransactionStatus;
+import org.testng.Assert;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
@@ -46,8 +55,281 @@ import static org.testng.Assert.assertTrue;
public class TestInvoicePayment extends TestIntegrationBase {
@Test(groups = "slow")
- public void testPartialPayments() throws Exception {
+ public void testPartialPaymentByPaymentPlugin() throws Exception {
+ // 2012-05-01T00:03:42.000Z
+ clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+ final AccountData accountData = getAccountData(0);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+ 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);
+
+ // Trigger a partial payment on the next invoice
+ paymentPlugin.overrideNextProcessedAmount(BigDecimal.TEN);
+
+ // 2012-05-31 => DAY 30 have to get out of trial {I0, P0}
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ Invoice invoice2 = 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);
+
+ // Invoice is partially paid
+ final Payment payment1 = paymentChecker.checkPayment(account.getId(), 1, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 5, 31), new BigDecimal("249.95"), TransactionStatus.SUCCESS, invoice2.getId(), Currency.USD));
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(invoice2.getBalance().compareTo(new BigDecimal("239.95")), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+ // 2012-06-30
+ addDaysAndCheckForCompletion(30, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ Invoice invoice3 = 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);
+
+ // Invoice is fully paid
+ final Payment payment2 = paymentChecker.checkPayment(account.getId(), 2, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 6, 30), new BigDecimal("249.95"), TransactionStatus.SUCCESS, invoice3.getId(), Currency.USD));
+ Assert.assertEquals(payment2.getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment2.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ invoice2 = invoiceUserApi.getInvoice(invoice2.getId(), callContext);
+ Assert.assertEquals(invoice2.getBalance().compareTo(new BigDecimal("239.95")), 0);
+ Assert.assertEquals(invoice3.getBalance().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+ // Fully pay the second invoice
+ final Payment payment3 = createPaymentAndCheckForCompletion(account, invoice2, invoice2.getBalance(), account.getCurrency(), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ paymentChecker.checkPayment(account.getId(), 3, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 6, 30), new BigDecimal("239.95"), TransactionStatus.SUCCESS, invoice2.getId(), Currency.USD));
+ Assert.assertEquals(payment3.getPurchasedAmount().compareTo(new BigDecimal("239.95")), 0);
+ Assert.assertEquals(payment3.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("239.95")), 0);
+ invoice2 = invoiceUserApi.getInvoice(invoice2.getId(), callContext);
+ invoice3 = invoiceUserApi.getInvoice(invoice3.getId(), callContext);
+ Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(invoice3.getBalance().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.ZERO), 0);
+ }
+
+ @Test(groups = "slow")
+ public void testPartialRefunds() throws Exception {
+ // 2012-05-01T00:03:42.000Z
+ clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+ final AccountData accountData = getAccountData(0);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+ 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 {I0, P0}
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ Invoice invoice2 = 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);
+
+ // Invoice is fully paid
+ Payment payment1 = paymentChecker.checkPayment(account.getId(), 1, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 5, 31), new BigDecimal("249.95"), TransactionStatus.SUCCESS, invoice2.getId(), Currency.USD));
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+ // Trigger first partial refund ($1), no adjustment
+ payment1 = refundPaymentAndCheckForCompletion(account, payment1, BigDecimal.TEN, Currency.USD, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getRefundedAmount().compareTo(BigDecimal.TEN), 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).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ invoice2 = invoiceUserApi.getInvoice(invoice2.getId(), callContext);
+ Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+ // Trigger second partial refund ($1), with item adjustment
+ final Map<UUID, BigDecimal> iias = new HashMap<UUID, BigDecimal>();
+ iias.put(invoice2.getInvoiceItems().get(0).getId(), BigDecimal.ONE);
+ payment1 = refundPaymentWithInvoiceItemAdjAndCheckForCompletion(account, payment1, BigDecimal.ONE, Currency.USD, iias, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.INVOICE_ADJUSTMENT);
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getRefundedAmount().compareTo(new BigDecimal("11")), 0);
+ Assert.assertEquals(payment1.getTransactions().size(), 3);
+ Assert.assertEquals(payment1.getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(2).getAmount().compareTo(BigDecimal.ONE), 0);
+ Assert.assertEquals(payment1.getTransactions().get(2).getProcessedAmount().compareTo(BigDecimal.ONE), 0);
+ invoice2 = invoiceUserApi.getInvoice(invoice2.getId(), callContext);
+ Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+ // Trigger third partial refund ($10), with item adjustment
+ iias.put(invoice2.getInvoiceItems().get(0).getId(), BigDecimal.TEN);
+ payment1 = refundPaymentWithInvoiceItemAdjAndCheckForCompletion(account, payment1, BigDecimal.TEN, Currency.USD, iias, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.INVOICE_ADJUSTMENT);
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getRefundedAmount().compareTo(new BigDecimal("21")), 0);
+ Assert.assertEquals(payment1.getTransactions().size(), 4);
+ Assert.assertEquals(payment1.getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(2).getAmount().compareTo(BigDecimal.ONE), 0);
+ Assert.assertEquals(payment1.getTransactions().get(2).getProcessedAmount().compareTo(BigDecimal.ONE), 0);
+ Assert.assertEquals(payment1.getTransactions().get(3).getAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(3).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ invoice2 = invoiceUserApi.getInvoice(invoice2.getId(), callContext);
+ Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+ }
+
+ @Test(groups = "slow")
+ public void testPartialPaymentByPaymentPluginThenChargeback() throws Exception {
+ // 2012-05-01T00:03:42.000Z
+ clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+ final AccountData accountData = getAccountData(0);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+ 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);
+
+ // Trigger a partial payment on the next invoice
+ paymentPlugin.overrideNextProcessedAmount(BigDecimal.TEN);
+
+ // 2012-05-31 => DAY 30 have to get out of trial {I0, P0}
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ Invoice invoice2 = 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);
+
+ // Invoice is partially paid
+ Payment payment1 = paymentChecker.checkPayment(account.getId(), 1, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 5, 31), new BigDecimal("249.95"), TransactionStatus.SUCCESS, invoice2.getId(), Currency.USD));
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().size(), 1);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(invoice2.getBalance().compareTo(new BigDecimal("239.95")), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+ // Trigger chargeback
+ payment1 = createChargeBackAndCheckForCompletion(account, payment1, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(BigDecimal.TEN), 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).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ invoice2 = invoiceUserApi.getInvoice(invoice2.getId(), callContext);
+ Assert.assertEquals(invoice2.getBalance().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+ }
+
+ @Test(groups = "slow")
+ public void testAUTO_PAY_OFFThenPartialPayment() throws Exception {
+ // 2012-05-01T00:03:42.000Z
+ clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+ final AccountData accountData = getAccountData(0);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+ 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);
+
+ // Put the account in AUTO_PAY_OFF to make sure payment system does not try to pay the initial invoice
+ add_AUTO_PAY_OFF_Tag(account.getId(), ObjectType.ACCOUNT);
+
+ // 2012-05-31 => DAY 30 have to get out of trial {I0, P0}
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE);
+
+ Invoice invoice2 = 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);
+
+ // Invoice is not paid
+ Assert.assertEquals(paymentApi.getAccountPayments(account.getId(), false, ImmutableList.<PluginProperty>of(), callContext).size(), 0);
+ Assert.assertEquals(invoice2.getBalance().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+ // Trigger partial payment
+ final Payment payment1 = createPaymentAndCheckForCompletion(account, invoice2, BigDecimal.TEN, account.getCurrency(), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ paymentChecker.checkPayment(account.getId(), 1, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 5, 31), BigDecimal.TEN, TransactionStatus.SUCCESS, invoice2.getId(), Currency.USD));
+ Assert.assertEquals(payment1.getTransactions().size(), 1);
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ invoice2 = invoiceUserApi.getInvoice(invoice2.getId(), callContext);
+ Assert.assertEquals(invoice2.getBalance().compareTo(new BigDecimal("239.95")), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+ // Remove AUTO_PAY_OFF and verify the invoice is fully paid
+ remove_AUTO_PAY_OFF_Tag(account.getId(), ObjectType.ACCOUNT, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ final Payment payment2 = paymentChecker.checkPayment(account.getId(), 2, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 5, 31), new BigDecimal("239.95"), TransactionStatus.SUCCESS, invoice2.getId(), Currency.USD));
+ Assert.assertEquals(payment2.getTransactions().size(), 1);
+ Assert.assertEquals(payment2.getPurchasedAmount().compareTo(new BigDecimal("239.95")), 0);
+ Assert.assertEquals(payment2.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("239.95")), 0);
+ invoice2 = invoiceUserApi.getInvoice(invoice2.getId(), callContext);
+ Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+ }
+
+ @Test(groups = "slow")
+ public void testPaymentDifferentCurrencyByPaymentPlugin() throws Exception {
+ // 2012-05-01T00:03:42.000Z
+ clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+ final AccountData accountData = getAccountData(0);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+ 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);
+
+ // Trigger a payment on the next invoice with a different currency ($249.95 <-> 225.44€)
+ paymentPlugin.overrideNextProcessedAmount(new BigDecimal("225.44"));
+ paymentPlugin.overrideNextProcessedCurrency(Currency.EUR);
+
+ // 2012-05-31 => DAY 30 have to get out of trial {I0, P0}
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ Invoice invoice2 = 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);
+
+ // Invoice is fully paid
+ Payment payment1 = paymentChecker.checkPayment(account.getId(), 1, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 5, 31), new BigDecimal("249.95"), TransactionStatus.SUCCESS, invoice2.getId(), Currency.USD));
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getTransactions().size(), 1);
+ Assert.assertEquals(payment1.getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(0).getCurrency(), Currency.USD);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("225.44")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedCurrency(), Currency.EUR);
+ Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.ZERO), 0);
+
+ // 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("249.95")), 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);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("225.44")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedCurrency(), Currency.EUR);
+ Assert.assertEquals(payment1.getTransactions().get(1).getAmount().compareTo(new BigDecimal("225.44")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getCurrency(), Currency.EUR);
+ Assert.assertEquals(payment1.getTransactions().get(1).getProcessedAmount().compareTo(new BigDecimal("225.44")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getProcessedCurrency(), Currency.EUR);
+ invoice2 = invoiceUserApi.getInvoice(invoice2.getId(), callContext);
+ Assert.assertEquals(invoice2.getBalance().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+ }
+
+ @Test(groups = "slow")
+ public void testPartialRefundsOnPartialPayments() throws Exception {
final AccountData accountData = getAccountData(1);
final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
accountChecker.checkAccount(account.getId(), accountData, callContext);
@@ -61,8 +343,9 @@ public class TestInvoicePayment extends TestIntegrationBase {
final InvoiceItem item1 = invoiceUserApi.insertExternalCharges(account.getId(), clock.getUTCToday(), ImmutableList.<InvoiceItem>of(externalCharge), callContext).get(0);
assertListenerStatus();
+ // Trigger first partial payment ($4) on first invoice
final Invoice invoice = invoiceUserApi.getInvoice(item1.getInvoiceId(), callContext);
- final Payment payment1 = createPaymentAndCheckForCompletion(account, invoice, new BigDecimal("4.00"), account.getCurrency(), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ Payment payment1 = createPaymentAndCheckForCompletion(account, invoice, new BigDecimal("4.00"), account.getCurrency(), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
Invoice invoice1 = invoiceUserApi.getInvoice(item1.getInvoiceId(), callContext);
assertTrue(invoice1.getBalance().compareTo(new BigDecimal("6.00")) == 0);
@@ -72,7 +355,8 @@ public class TestInvoicePayment extends TestIntegrationBase {
BigDecimal accountBalance = invoiceUserApi.getAccountBalance(account.getId(), callContext);
assertTrue(accountBalance.compareTo(new BigDecimal("6.00")) == 0);
- final Payment payment2 = createPaymentAndCheckForCompletion(account, invoice, new BigDecimal("6.00"), account.getCurrency(), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ // Trigger second partial payment ($6) on first invoice
+ Payment payment2 = createPaymentAndCheckForCompletion(account, invoice, new BigDecimal("6.00"), account.getCurrency(), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
invoice1 = invoiceUserApi.getInvoice(item1.getInvoiceId(), callContext);
assertTrue(invoice1.getBalance().compareTo(BigDecimal.ZERO) == 0);
@@ -82,35 +366,60 @@ public class TestInvoicePayment extends TestIntegrationBase {
accountBalance = invoiceUserApi.getAccountBalance(account.getId(), callContext);
assertTrue(accountBalance.compareTo(BigDecimal.ZERO) == 0);
-/*
- This does not work since item is paid across multiple payments and so the mount is bigger than the payment.
-
- // Now, issue refund with item adjustment on first invoice/item
- paymentApi.createRefundWithItemsAdjustments(account, payment1.getId(), Sets.<UUID>newHashSet(item1.getId()), callContext);
-
+ // Refund first payment with item adjustment
+ final Map<UUID, BigDecimal> iias = new HashMap<UUID, BigDecimal>();
+ iias.put(item1.getId(), new BigDecimal("4.00"));
+ payment1 = refundPaymentWithInvoiceItemAdjAndCheckForCompletion(account, payment1, new BigDecimal("4.00"), Currency.USD, iias, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.INVOICE_ADJUSTMENT);
invoice1 = invoiceUserApi.getInvoice(item1.getInvoiceId(), callContext);
assertTrue(invoice1.getBalance().compareTo(BigDecimal.ZERO) == 0);
-
accountBalance = invoiceUserApi.getAccountBalance(account.getId(), callContext);
assertTrue(accountBalance.compareTo(BigDecimal.ZERO) == 0);
-*/
-
- refundPaymentAndCheckForCompletion(account, payment1, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
-
+ // Refund second payment with item adjustment
+ iias.put(item1.getId(), new BigDecimal("6.00"));
+ payment2 = refundPaymentWithInvoiceItemAdjAndCheckForCompletion(account, payment2, new BigDecimal("6.00"), Currency.USD, iias, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.INVOICE_ADJUSTMENT);
invoice1 = invoiceUserApi.getInvoice(item1.getInvoiceId(), callContext);
- assertTrue(invoice1.getBalance().compareTo(new BigDecimal("4.00")) == 0);
-
+ assertTrue(invoice1.getBalance().compareTo(BigDecimal.ZERO) == 0);
accountBalance = invoiceUserApi.getAccountBalance(account.getId(), callContext);
- assertTrue(accountBalance.compareTo(new BigDecimal("4.00")) == 0);
+ assertTrue(accountBalance.compareTo(BigDecimal.ZERO) == 0);
+ Assert.assertEquals(invoice1.getPayments().size(), 4);
+
+ // Verify links for payment 1
+ Assert.assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(new BigDecimal("4.00")), 0);
+ Assert.assertNull(invoice1.getPayments().get(0).getLinkedInvoicePaymentId());
+ Assert.assertNull(invoice1.getPayments().get(0).getPaymentCookieId());
+ Assert.assertEquals(invoice1.getPayments().get(0).getPaymentId(), payment1.getId());
+ Assert.assertEquals(invoice1.getPayments().get(0).getType(), InvoicePaymentType.ATTEMPT);
+ Assert.assertTrue(invoice1.getPayments().get(0).isSuccess());
+
+ // Verify links for payment 2
+ Assert.assertEquals(invoice1.getPayments().get(1).getAmount().compareTo(new BigDecimal("6.00")), 0);
+ Assert.assertNull(invoice1.getPayments().get(1).getLinkedInvoicePaymentId());
+ Assert.assertNull(invoice1.getPayments().get(1).getPaymentCookieId());
+ Assert.assertEquals(invoice1.getPayments().get(1).getPaymentId(), payment2.getId());
+ Assert.assertEquals(invoice1.getPayments().get(1).getType(), InvoicePaymentType.ATTEMPT);
+ Assert.assertTrue(invoice1.getPayments().get(1).isSuccess());
+
+ // Verify links for refund 1
+ Assert.assertEquals(invoice1.getPayments().get(2).getAmount().compareTo(new BigDecimal("-4.00")), 0);
+ Assert.assertEquals(invoice1.getPayments().get(2).getLinkedInvoicePaymentId(), invoice1.getPayments().get(0).getId());
+ Assert.assertEquals(invoice1.getPayments().get(2).getPaymentCookieId(), payment1.getTransactions().get(1).getExternalKey());
+ Assert.assertEquals(invoice1.getPayments().get(2).getPaymentId(), payment1.getId());
+ Assert.assertEquals(invoice1.getPayments().get(2).getType(), InvoicePaymentType.REFUND);
+ Assert.assertTrue(invoice1.getPayments().get(2).isSuccess());
+
+ // Verify links for refund 2
+ Assert.assertEquals(invoice1.getPayments().get(3).getAmount().compareTo(new BigDecimal("-6.00")), 0);
+ Assert.assertEquals(invoice1.getPayments().get(3).getLinkedInvoicePaymentId(), invoice1.getPayments().get(1).getId());
+ Assert.assertEquals(invoice1.getPayments().get(3).getPaymentCookieId(), payment2.getTransactions().get(1).getExternalKey());
+ Assert.assertEquals(invoice1.getPayments().get(3).getPaymentId(), payment2.getId());
+ Assert.assertEquals(invoice1.getPayments().get(3).getType(), InvoicePaymentType.REFUND);
+ Assert.assertTrue(invoice1.getPayments().get(3).isSuccess());
}
- //
-
@Test(groups = "slow")
public void testWithPaymentFailure() throws Exception {
-
clock.setDay(new LocalDate(2012, 4, 1));
final AccountData accountData = getAccountData(1);
@@ -129,34 +438,48 @@ public class TestInvoicePayment extends TestIntegrationBase {
assertEquals(invoices.size(), 2);
final Invoice invoice1 = invoices.get(0).getInvoiceItems().get(0).getInvoiceItemType() == InvoiceItemType.RECURRING ?
- invoices.get(0) : invoices.get(1);
+ invoices.get(0) : invoices.get(1);
assertTrue(invoice1.getBalance().compareTo(new BigDecimal("249.95")) == 0);
assertTrue(invoice1.getPaidAmount().compareTo(BigDecimal.ZERO) == 0);
assertTrue(invoice1.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
assertEquals(invoice1.getPayments().size(), 1);
+ assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(invoice1.getPayments().get(0).getCurrency(), Currency.USD);
assertFalse(invoice1.getPayments().get(0).isSuccess());
- BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ final BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
assertTrue(accountBalance1.compareTo(new BigDecimal("249.95")) == 0);
final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, ImmutableList.<PluginProperty>of(), callContext);
assertEquals(payments.size(), 1);
+ assertEquals(payments.get(0).getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments.get(0).getTransactions().size(), 1);
+ assertEquals(payments.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
// Trigger the payment retry
busHandler.pushExpectedEvents(NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
clock.addDays(8);
assertListenerStatus();
- Invoice invoice2 = invoiceUserApi.getInvoice(invoice1.getId(), callContext);
+ final Invoice invoice2 = invoiceUserApi.getInvoice(invoice1.getId(), callContext);
assertTrue(invoice2.getBalance().compareTo(BigDecimal.ZERO) == 0);
assertTrue(invoice2.getPaidAmount().compareTo(new BigDecimal("249.95")) == 0);
assertTrue(invoice2.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
assertEquals(invoice2.getPayments().size(), 1);
assertTrue(invoice2.getPayments().get(0).isSuccess());
- BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ final BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
assertTrue(accountBalance2.compareTo(BigDecimal.ZERO) == 0);
- }
-
+ final List<Payment> payments2 = paymentApi.getAccountPayments(account.getId(), false, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments2.size(), 1);
+ assertEquals(payments2.get(0).getTransactions().size(), 2);
+ assertEquals(payments2.get(0).getTransactions().get(1).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(1).getCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(1).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(1).getProcessedCurrency(), Currency.USD);
+ }
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/util/InvoiceChecker.java b/beatrix/src/test/java/org/killbill/billing/beatrix/util/InvoiceChecker.java
index 77bc381..7a10f54 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/util/InvoiceChecker.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/util/InvoiceChecker.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 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:
*
@@ -159,17 +161,11 @@ public class InvoiceChecker {
if (expectedLocalCTD == null) {
assertNull(subscription.getChargedThroughDate());
} else {
- assertTrue(expectedLocalCTD.compareTo(subscription.getChargedThroughDate().toLocalDate()) == 0);
- /*
- final DateTime expectedCTD = expectedLocalCTD.toDateTime(new LocalTime(subscription.getStartDate().getMillis(), DateTimeZone.UTC), DateTimeZone.UTC);
- final String msg = String.format("Checking CTD for entitlement %s : expectedLocalCTD = %s => expectedCTD = %s, got %s",
- entitlementId, expectedLocalCTD, expectedCTD, subscription.getChargedThroughDate());
- log.info(msg);
- assertNotNull(subscription.getChargedThroughDate());
- assertTrue(subscription.getChargedThroughDate().compareTo(expectedCTD) == 0, msg);
- */
+ final String msg = String.format("Checking CTD for entitlement %s : expectedLocalCTD = %s, got %s",
+ entitlementId, expectedLocalCTD, subscription.getChargedThroughDate().toLocalDate());
+ assertTrue(expectedLocalCTD.compareTo(subscription.getChargedThroughDate().toLocalDate()) == 0, msg);
}
- } catch (EntitlementApiException e) {
+ } catch (final EntitlementApiException e) {
fail("Failed to retrieve entitlement for " + entitlementId);
}
}
@@ -182,7 +178,7 @@ public class InvoiceChecker {
private final InvoiceItemType type;
private final BigDecimal amount;
- public ExpectedInvoiceItemCheck(final InvoiceItemType type, final BigDecimal amount, boolean checkDates) {
+ public ExpectedInvoiceItemCheck(final InvoiceItemType type, final BigDecimal amount, final boolean checkDates) {
this.checkDates = checkDates;
this.type = type;
this.startDate = null;
@@ -195,7 +191,7 @@ public class InvoiceChecker {
}
public ExpectedInvoiceItemCheck(final LocalDate startDate, final LocalDate endDate,
- final InvoiceItemType type, final BigDecimal amount, boolean checkDates) {
+ final InvoiceItemType type, final BigDecimal amount, final boolean checkDates) {
this.checkDates = checkDates;
this.startDate = startDate;
this.endDate = endDate;
@@ -227,6 +223,17 @@ public class InvoiceChecker {
public BigDecimal getAmount() {
return amount;
}
- }
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("ExpectedInvoiceItemCheck{");
+ sb.append("checkDates=").append(checkDates);
+ sb.append(", startDate=").append(startDate);
+ sb.append(", endDate=").append(endDate);
+ sb.append(", type=").append(type);
+ sb.append(", amount=").append(amount);
+ sb.append('}');
+ return sb.toString();
+ }
+ }
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/util/PaymentChecker.java b/beatrix/src/test/java/org/killbill/billing/beatrix/util/PaymentChecker.java
index 69cfe82..efaf1ea 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/util/PaymentChecker.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/util/PaymentChecker.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
@@ -24,6 +24,8 @@ import java.util.UUID;
import org.joda.time.LocalDate;
import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentApi;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PaymentApi;
import org.killbill.billing.payment.api.PaymentTransaction;
@@ -46,15 +48,17 @@ public class PaymentChecker {
private static final Logger log = LoggerFactory.getLogger(PaymentChecker.class);
private final PaymentApi paymentApi;
+ private final InvoicePaymentApi invoicePaymentApi;
private final AuditChecker auditChecker;
@Inject
- public PaymentChecker(final PaymentApi paymentApi, final AuditChecker auditChecker) {
+ public PaymentChecker(final PaymentApi paymentApi, final InvoicePaymentApi invoicePaymentApi, final AuditChecker auditChecker) {
this.paymentApi = paymentApi;
+ this.invoicePaymentApi = invoicePaymentApi;
this.auditChecker = auditChecker;
}
- public Payment checkPayment(final UUID accountId, final int paymentOrderingNumber, final CallContext context, ExpectedPaymentCheck expected) throws PaymentApiException {
+ public Payment checkPayment(final UUID accountId, final int paymentOrderingNumber, final CallContext context, final ExpectedPaymentCheck expected) throws PaymentApiException {
final List<Payment> payments = paymentApi.getAccountPayments(accountId, false, ImmutableList.<PluginProperty>of(), context);
Assert.assertEquals(payments.size(), paymentOrderingNumber);
final Payment payment = payments.get(paymentOrderingNumber - 1);
@@ -78,10 +82,19 @@ public class PaymentChecker {
private void checkPayment(final UUID accountId, final Payment payment, final CallContext context, final ExpectedPaymentCheck expected) {
Assert.assertEquals(payment.getAccountId(), accountId);
+ Assert.assertEquals(payment.getCurrency(), expected.getCurrency());
+
+ if (expected.getInvoiceId() != null) {
+ for (final InvoicePayment invoicePayment : invoicePaymentApi.getInvoicePayments(payment.getId(), context)) {
+ Assert.assertEquals(invoicePayment.getInvoiceId(), expected.getInvoiceId());
+ }
+ }
+
final PaymentTransaction transaction = getPurchaseTransaction(payment);
- Assert.assertTrue(transaction.getAmount().compareTo(expected.getAmount()) == 0);
+ Assert.assertTrue(transaction.getAmount().compareTo(expected.getAmount()) == 0, "Actual amount " + transaction.getAmount() + ", expected amount " + expected.getAmount());
Assert.assertEquals(transaction.getTransactionStatus(), expected.getStatus());
- Assert.assertEquals(payment.getCurrency(), expected.getCurrency());
+ Assert.assertEquals(transaction.getEffectiveDate().toLocalDate().compareTo(expected.getPaymentDate()), 0);
+
auditChecker.checkPaymentCreated(payment, context);
}
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 a4d2148..93462b9 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
@@ -288,44 +288,55 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
// Get the latest state from disk
refresh(callContext);
- if (eventsStream.isEntitlementCancelled()) {
- throw new EntitlementApiException(ErrorCode.SUB_CANCEL_BAD_STATE, getId(), EntitlementState.CANCELLED);
- }
+ final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.CANCEL_SUBSCRIPTION,
+ getAccountId(),
+ null,
+ getBundleId(),
+ getExternalKey(),
+ null,
+ localCancelDate,
+ properties,
+ callContext);
- final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(getAccountId(), callContext);
- final DateTime effectiveCancelDate = dateHelper.fromLocalDateAndReferenceTime(localCancelDate, getSubscriptionBase().getStartDate(), contextWithValidAccountRecordId);
- try {
- if (overrideBillingEffectiveDate) {
- getSubscriptionBase().cancelWithDate(effectiveCancelDate, callContext);
- } else {
- getSubscriptionBase().cancel(callContext);
- }
- } catch (final SubscriptionBaseApiException e) {
- throw new EntitlementApiException(e);
- }
- final BlockingState newBlockingState = new DefaultBlockingState(getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, effectiveCancelDate);
- final Collection<NotificationEvent> notificationEvents = new ArrayList<NotificationEvent>();
- final Collection<BlockingState> addOnsBlockingStates = computeAddOnBlockingStates(effectiveCancelDate, notificationEvents, callContext, contextWithValidAccountRecordId);
+ final WithEntitlementPlugin<Entitlement> cancelEntitlementWithPlugin = new WithEntitlementPlugin<Entitlement>() {
- // Record the new state first, then insert the notifications to avoid race conditions
- setBlockingStates(newBlockingState, addOnsBlockingStates, contextWithValidAccountRecordId);
- for (final NotificationEvent notificationEvent : notificationEvents) {
- recordFutureNotification(effectiveCancelDate, notificationEvent, contextWithValidAccountRecordId);
- }
+ @Override
+ public Entitlement doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+ if (eventsStream.isEntitlementCancelled()) {
+ throw new EntitlementApiException(ErrorCode.SUB_CANCEL_BAD_STATE, getId(), EntitlementState.CANCELLED);
+ }
- return entitlementApi.getEntitlementForId(getId(), callContext);
- }
+ final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(getAccountId(), callContext);
+ final DateTime effectiveCancelDate = dateHelper.fromLocalDateAndReferenceTime(localCancelDate, getSubscriptionBase().getStartDate(), contextWithValidAccountRecordId);
+ try {
+ if (overrideBillingEffectiveDate) {
+ getSubscriptionBase().cancelWithDate(effectiveCancelDate, callContext);
+ } else {
+ getSubscriptionBase().cancel(callContext);
+ }
+ } catch (final SubscriptionBaseApiException e) {
+ throw new EntitlementApiException(e);
+ }
- @Override
- public Entitlement cancelEntitlementWithPolicyOverrideBillingPolicy(final EntitlementActionPolicy entitlementPolicy, final BillingActionPolicy billingPolicy, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
- // Get the latest state from disk - required to have the latest CTD
- refresh(callContext);
+ final BlockingState newBlockingState = new DefaultBlockingState(getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, effectiveCancelDate);
+ final Collection<NotificationEvent> notificationEvents = new ArrayList<NotificationEvent>();
+ final Collection<BlockingState> addOnsBlockingStates = computeAddOnBlockingStates(effectiveCancelDate, notificationEvents, callContext, contextWithValidAccountRecordId);
- final LocalDate cancellationDate = getLocalDateFromEntitlementPolicy(entitlementPolicy);
- return cancelEntitlementWithDateOverrideBillingPolicy(cancellationDate, billingPolicy, properties, callContext);
+ // Record the new state first, then insert the notifications to avoid race conditions
+ setBlockingStates(newBlockingState, addOnsBlockingStates, contextWithValidAccountRecordId);
+ for (final NotificationEvent notificationEvent : notificationEvents) {
+ recordFutureNotification(effectiveCancelDate, notificationEvent, contextWithValidAccountRecordId);
+ }
+
+ return entitlementApi.getEntitlementForId(getId(), callContext);
+ }
+ };
+
+ return pluginExecution.executeWithPlugin(cancelEntitlementWithPlugin, pluginContext);
}
+
@Override
public void uncancelEntitlement(final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
@@ -368,6 +379,16 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
}
}
+ @Override
+ public Entitlement cancelEntitlementWithPolicyOverrideBillingPolicy(final EntitlementActionPolicy entitlementPolicy, final BillingActionPolicy billingPolicy, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
+ // Get the latest state from disk - required to have the latest CTD
+ refresh(callContext);
+
+ final LocalDate cancellationDate = getLocalDateFromEntitlementPolicy(entitlementPolicy);
+ return cancelEntitlementWithDateOverrideBillingPolicy(cancellationDate, billingPolicy, properties, callContext);
+ }
+
+
// See also EntitlementInternalApi#cancel for the bulk API
@Override
public Entitlement cancelEntitlementWithDateOverrideBillingPolicy(final LocalDate localCancelDate, final BillingActionPolicy billingPolicy, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
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 0173d7c..59f2c89 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
@@ -168,11 +168,6 @@ public class DefaultEntitlementApiBase {
public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
try {
- final BlockingState currentState = blockingStateDao.getBlockingStateForService(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, EntitlementService.ENTITLEMENT_SERVICE_NAME, internalCallContext);
- if (currentState != null && currentState.getStateName().equals(DefaultEntitlementApi.ENT_STATE_BLOCKED)) {
- throw new EntitlementApiException(ErrorCode.ENT_ALREADY_BLOCKED, bundleId);
- }
-
final SubscriptionBaseBundle bundle = subscriptionInternalApi.getBundleFromId(bundleId, internalCallContext);
final ImmutableAccountData account = accountApi.getImmutableAccountDataById(bundle.getAccountId(), internalCallContext);
final SubscriptionBase baseSubscription = subscriptionInternalApi.getBaseSubscription(bundleId, internalCallContext);
@@ -235,13 +230,6 @@ public class DefaultEntitlementApiBase {
return null;
}
- final BlockingState currentState = blockingStateDao.getBlockingStateForService(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, EntitlementService.ENTITLEMENT_SERVICE_NAME, internalCallContext);
- if (currentState == null || currentState.getStateName().equals(DefaultEntitlementApi.ENT_STATE_CLEAR)) {
- // Nothing to do.
- log.warn("Current state is {}, nothing to resume", currentState);
- return null;
- }
-
final UUID blockingId = blockUnblockBundle(bundleId, DefaultEntitlementApi.ENT_STATE_CLEAR, EntitlementService.ENTITLEMENT_SERVICE_NAME, localEffectiveDate, false, false, false, baseSubscription, internalCallContext);
// Should we send one event per entitlement in the bundle?
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java
index 971786e..007c423 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java
@@ -21,6 +21,7 @@ import java.util.UUID;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.overdue.OverdueService;
import org.killbill.clock.Clock;
import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
@@ -46,12 +47,7 @@ public class DefaultInternalBlockingApi implements BlockingInternalApi {
@Override
public BlockingState getBlockingStateForService(final UUID overdueableId, final BlockingStateType blockingStateType, final String serviceName, final InternalTenantContext context) {
- final BlockingState blockingStateForService = dao.getBlockingStateForService(overdueableId, blockingStateType, serviceName, context);
- if (blockingStateForService == null) {
- return DefaultBlockingState.getClearState(blockingStateType, serviceName, clock);
- } else {
- return blockingStateForService;
- }
+ return dao.getBlockingStateForService(overdueableId, blockingStateType, serviceName, context);
}
@Override
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 9a397ae..74fda0c 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
@@ -311,14 +311,6 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
final Entitlement telescopicEntitlement2 = entitlementApi.getEntitlementForId(telescopicEntitlement.getId(), callContext);
assertEquals(telescopicEntitlement2.getState(), EntitlementState.BLOCKED);
- // Check we can't block in a blocked state
- try {
- entitlementApi.pause(baseEntitlement.getBundleId(), new LocalDate(clock.getUTCNow()), ImmutableList.<PluginProperty>of(), callContext);
- Assert.fail("Should not have succeeded to block in a blocked state");
- } catch (EntitlementApiException e) {
- assertEquals(e.getCode(), ErrorCode.ENT_ALREADY_BLOCKED.getCode());
- }
-
final List<Entitlement> bundleEntitlements2 = entitlementApi.getAllEntitlementsForBundle(telescopicEntitlement2.getBundleId(), callContext);
assertEquals(bundleEntitlements2.size(), 2);
for (final Entitlement cur : bundleEntitlements2) {
@@ -339,8 +331,11 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
entitlementApi.resume(baseEntitlement.getBundleId(), new LocalDate(clock.getUTCNow()), ImmutableList.<PluginProperty>of(), callContext);
assertListenerStatus();
- // Verify call is idempotent
+ // Verify call is idempotent : The current semantics is to post the RESUME because we went through the operation, but not the BLOCK because the DAO logic
+ // filtered the event as the subscription was already resumed.
+ testListener.pushExpectedEvents(NextEvent.RESUME);
entitlementApi.resume(baseEntitlement.getBundleId(), new LocalDate(clock.getUTCNow()), ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
// Verify blocking state
final Entitlement baseEntitlement3 = entitlementApi.getEntitlementForId(baseEntitlement.getId(), callContext);
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestProxyBlockingStateDao.java b/entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestProxyBlockingStateDao.java
index 549f02b..8c93f3a 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestProxyBlockingStateDao.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestProxyBlockingStateDao.java
@@ -78,7 +78,7 @@ public class TestProxyBlockingStateDao extends EntitlementTestSuiteNoDB {
final BlockingState bs4 = new DefaultBlockingState(UUID.randomUUID(),
blockedId,
blockingStateType,
- DefaultBlockingState.CLEAR_STATE_NAME,
+ "OD4",
service,
false,
false,
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
index 80472a9..56565d2 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
@@ -20,7 +20,6 @@ package org.killbill.billing.invoice.api.svcs;
import java.math.BigDecimal;
import java.util.Collection;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -111,11 +110,6 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
}
@Override
- public InvoicePayment getInvoicePaymentForRefund(final UUID paymentId, final InternalTenantContext context) throws InvoiceApiException {
- return getInvoicePayment(paymentId, InvoicePaymentType.REFUND, context);
- }
-
- @Override
public InvoicePayment getInvoicePaymentForChargeback(final UUID paymentId, final InternalTenantContext context) throws InvoiceApiException {
return getInvoicePayment(paymentId, InvoicePaymentType.CHARGED_BACK, context);
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
index c81c6e8..9e1cd9d 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -256,7 +256,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
}
return new ExternalChargeInvoiceItem(externalChargeItem.getId(), externalChargeItem.getInvoiceId(), externalChargeItem.getAccountId(),
- externalChargeItem.getPlanName(), externalChargeItem.getStartDate(),
+ externalChargeItem.getDescription(), externalChargeItem.getStartDate(),
externalChargeItem.getAmount(), externalChargeItem.getCurrency());
}
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 f22f497..5166e98 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
@@ -542,7 +542,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
}
// We expect the code to correctly pass the account currency -- the payment code, more generic accept chargeBack in different currencies,
// but this is only for direct payment (no invoice)
- Preconditions.checkArgument(invoicePayment.getCurrency() == currency);
+ Preconditions.checkArgument(invoicePayment.getCurrency() == currency, String.format("Invoice payment currency %s doesn't match chargeback currency %s", invoicePayment.getCurrency(), currency));
final UUID invoicePaymentId = invoicePayment.getId();
final BigDecimal maxChargedBackAmount = invoiceDaoHelper.getRemainingAmountPaidFromTransaction(invoicePaymentId, entitySqlDaoWrapperFactory, 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 e80ffa1..acf13dc 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
@@ -178,7 +178,7 @@ public class InvoiceDaoHelper {
public boolean apply(final InvoiceModelDao in) {
final BigDecimal balance = InvoiceModelDaoHelper.getBalance(in);
log.debug("Computed balance={} for invoice={}", balance, in);
- return (balance.compareTo(BigDecimal.ZERO) >= 1) && (upToDate == null || !in.getTargetDate().isAfter(upToDate));
+ return (!in.isWrittenOff() && balance.compareTo(BigDecimal.ZERO) >= 1) && (upToDate == null || !in.getTargetDate().isAfter(upToDate));
}
});
return new ArrayList<InvoiceModelDao>(unpaidInvoices);
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql b/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql
index 1a3907c..f7cda2b 100644
--- a/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql
@@ -72,7 +72,6 @@ CREATE TABLE invoice_payments (
PRIMARY KEY(record_id)
) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
CREATE UNIQUE INDEX invoice_payments_id ON invoice_payments(id);
-CREATE UNIQUE INDEX idx_invoice_payments ON invoice_payments(payment_id, type);
CREATE INDEX invoice_payments_invoice_id ON invoice_payments(invoice_id);
CREATE INDEX invoice_payments_reversals ON invoice_payments(linked_invoice_payment_id);
CREATE INDEX invoice_payments_tenant_account_record_id ON invoice_payments(tenant_record_id, account_record_id);
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 a572def..2fd989a 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
@@ -70,9 +70,11 @@ public class TestDefaultInvoiceUserApi extends InvoiceTestSuiteWithEmbeddedDB {
// Post an external charge
final BigDecimal externalChargeAmount = BigDecimal.TEN;
- final InvoiceItem externalCharge = new ExternalChargeInvoiceItem(null, accountId, null, UUID.randomUUID().toString(), clock.getUTCToday(), externalChargeAmount, accountCurrency);
+ final InvoiceItem externalCharge = new ExternalChargeInvoiceItem(null, accountId, null, "description", clock.getUTCToday(), externalChargeAmount, accountCurrency);
final InvoiceItem externalChargeInvoiceItem = invoiceUserApi.insertExternalCharges(accountId, clock.getUTCToday(), ImmutableList.<InvoiceItem>of(externalCharge), callContext).get(0);
verifyExternalChargeOnNewInvoice(accountBalance, null, externalChargeAmount, externalChargeInvoiceItem);
+
+ assertEquals(externalChargeInvoiceItem.getDescription(), "description");
}
@Test(groups = "slow")
@@ -124,7 +126,7 @@ public class TestDefaultInvoiceUserApi extends InvoiceTestSuiteWithEmbeddedDB {
verifyExternalChargeOnExistingInvoice(invoiceBalance, null, externalChargeAmount, externalChargeInvoiceItem);
}
- @Test(groups = "slow", enabled = false)
+ @Test(groups = "slow")
public void testOriginalAmountCharged() throws Exception {
final Invoice initialInvoice = invoiceUserApi.getInvoice(invoiceId, callContext);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestProRation.java
index e56b43f..f1f0399 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestProRation.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestProRation.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 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:
*
@@ -16,17 +18,22 @@
package org.killbill.billing.invoice.proRations.inAdvance.annual;
-import static org.killbill.billing.invoice.TestInvoiceHelper.*;
-
import java.math.BigDecimal;
import org.joda.time.LocalDate;
-import org.testng.annotations.Test;
-
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.invoice.model.InvalidDateSequenceException;
import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase;
import org.killbill.billing.util.currency.KillBillMoney;
+import org.testng.annotations.Test;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.FORTY_SEVEN;
+import static org.killbill.billing.invoice.TestInvoiceHelper.ONE;
+import static org.killbill.billing.invoice.TestInvoiceHelper.SEVEN;
+import static org.killbill.billing.invoice.TestInvoiceHelper.THREE_HUNDRED_AND_FIFTY_FOUR;
+import static org.killbill.billing.invoice.TestInvoiceHelper.THREE_HUNDRED_AND_FOURTY_NINE;
+import static org.killbill.billing.invoice.TestInvoiceHelper.THREE_HUNDRED_AND_SIXTY_FIVE;
+import static org.killbill.billing.invoice.TestInvoiceHelper.THREE_HUNDRED_AND_SIXTY_SIX;
public class TestProRation extends ProRationInAdvanceTestBase {
@@ -55,14 +62,17 @@ public class TestProRation extends ProRationInAdvanceTestBase {
testCalculateNumberOfBillingCycles(startDate, targetDate, 4, expectedValue);
}
- // TODO Test fails, needs to be investigated
- @Test(groups = "fast", enabled = false)
+ @Test(groups = "fast")
public void testSinglePlanDoubleProRation() throws InvalidDateSequenceException {
final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 10);
final LocalDate endDate = invoiceUtil.buildDate(2012, 3, 4);
final LocalDate targetDate = invoiceUtil.buildDate(2012, 4, 5);
- final BigDecimal expectedValue = BigDecimal.ZERO;
+ // SEVEN is number of days between startDate and expected first billing cycle date (2011, 1, 17);
+ // FORTY_SEVEN is number of days between the second billing cycle date (2012, 1, 17) and the end date (2012, 3, 4);
+ // 2011 has 365 days but 2012 has 366 days
+ final BigDecimal expectedValue = ONE.add(SEVEN.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD))
+ .add(FORTY_SEVEN.divide(THREE_HUNDRED_AND_SIXTY_SIX, KillBillMoney.ROUNDING_METHOD));
testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, expectedValue);
}
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
index 4e2d5f6..3cc1c21 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -128,6 +128,7 @@ public class TestInvoiceHelper {
public static final BigDecimal THIRTY_THREE = new BigDecimal("33.0").setScale(KillBillMoney.MAX_SCALE);
public static final BigDecimal FORTY = new BigDecimal("40.0").setScale(KillBillMoney.MAX_SCALE);
+ public static final BigDecimal FORTY_SEVEN = new BigDecimal("47.0").setScale(KillBillMoney.MAX_SCALE);
public static final BigDecimal SIXTY_SIX = new BigDecimal("66.0").setScale(KillBillMoney.MAX_SCALE);
public static final BigDecimal SEVENTY_FIVE = new BigDecimal("75.0").setScale(KillBillMoney.MAX_SCALE);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
index 79968cc..e286e85 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
@@ -383,13 +383,14 @@ public class PaymentResource extends ComboPaymentResource {
@ApiResponse(code = 404, message = "Account or payment not found")})
public Response captureAuthorization(final PaymentTransactionJson json,
@PathParam("paymentId") final String paymentIdStr,
+ @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List<String> paymentControlPluginNames,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
@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 PaymentApiException, AccountApiException {
- return captureAuthorizationInternal(json, paymentIdStr, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+ return captureAuthorizationInternal(json, paymentIdStr, paymentControlPluginNames, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
}
@TimedResource(name = "captureAuthorization")
@@ -399,17 +400,19 @@ public class PaymentResource extends ComboPaymentResource {
@ApiOperation(value = "Capture an existing authorization")
@ApiResponses(value = {@ApiResponse(code = 404, message = "Account or payment not found")})
public Response captureAuthorizationByExternalKey(final PaymentTransactionJson json,
+ @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List<String> paymentControlPluginNames,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
@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 PaymentApiException, AccountApiException {
- return captureAuthorizationInternal(json, null, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+ return captureAuthorizationInternal(json, null, paymentControlPluginNames, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
}
private Response captureAuthorizationInternal(final PaymentTransactionJson json,
@Nullable final String paymentIdStr,
+ final List<String> paymentControlPluginNames,
final List<String> pluginPropertiesString,
final String createdBy,
final String reason,
@@ -427,8 +430,10 @@ public class PaymentResource extends ComboPaymentResource {
final Account account = accountUserApi.getAccountById(initialPayment.getAccountId(), callContext);
final Currency currency = json.getCurrency() == null ? account.getCurrency() : Currency.valueOf(json.getCurrency());
- final Payment payment = paymentApi.createCapture(account, initialPayment.getId(), json.getAmount(), currency,
- json.getTransactionExternalKey(), pluginProperties, callContext);
+ final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
+
+ final Payment payment = paymentApi.createCaptureWithPaymentControl(account, initialPayment.getId(), json.getAmount(), currency,
+ json.getTransactionExternalKey(), pluginProperties, paymentOptions, callContext);
return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", payment.getId());
}
@@ -442,13 +447,14 @@ public class PaymentResource extends ComboPaymentResource {
@ApiResponse(code = 404, message = "Account or payment not found")})
public Response refundPayment(final PaymentTransactionJson json,
@PathParam("paymentId") final String paymentIdStr,
+ @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List<String> paymentControlPluginNames,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
@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 PaymentApiException, AccountApiException {
- return refundPaymentInternal(json, paymentIdStr, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+ return refundPaymentInternal(json, paymentIdStr, paymentControlPluginNames, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
}
@TimedResource(name = "refundPayment")
@@ -459,18 +465,20 @@ public class PaymentResource extends ComboPaymentResource {
@ApiOperation(value = "Refund an existing payment")
@ApiResponses(value = {@ApiResponse(code = 404, message = "Account or payment not found")})
public Response refundPaymentByExternalKey(final PaymentTransactionJson json,
+ @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List<String> paymentControlPluginNames,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
@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 PaymentApiException, AccountApiException {
- return refundPaymentInternal(json, null, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+ return refundPaymentInternal(json, null, paymentControlPluginNames, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
}
private Response refundPaymentInternal(final PaymentTransactionJson json,
@Nullable final String paymentIdStr,
+ final List<String> paymentControlPluginNames,
final List<String> pluginPropertiesString,
final String createdBy,
final String reason,
@@ -488,8 +496,10 @@ public class PaymentResource extends ComboPaymentResource {
final Account account = accountUserApi.getAccountById(initialPayment.getAccountId(), callContext);
final Currency currency = json.getCurrency() == null ? account.getCurrency() : Currency.valueOf(json.getCurrency());
- final Payment payment = paymentApi.createRefund(account, initialPayment.getId(), json.getAmount(), currency,
- json.getTransactionExternalKey(), pluginProperties, callContext);
+ final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
+
+ final Payment payment = paymentApi.createRefundWithPaymentControl(account, initialPayment.getId(), json.getAmount(), currency,
+ json.getTransactionExternalKey(), pluginProperties, paymentOptions, callContext);
return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", payment.getId());
}
@@ -504,13 +514,14 @@ public class PaymentResource extends ComboPaymentResource {
@ApiResponse(code = 404, message = "Account or payment not found")})
public Response voidPayment(final PaymentTransactionJson json,
@PathParam("paymentId") final String paymentIdStr,
+ @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List<String> paymentControlPluginNames,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
@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 PaymentApiException, AccountApiException {
- return voidPaymentInternal(json, paymentIdStr, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+ return voidPaymentInternal(json, paymentIdStr, paymentControlPluginNames, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
}
@TimedResource(name = "voidPayment")
@@ -520,17 +531,19 @@ public class PaymentResource extends ComboPaymentResource {
@ApiOperation(value = "Void an existing payment")
@ApiResponses(value = {@ApiResponse(code = 404, message = "Account or payment not found")})
public Response voidPaymentByExternalKey(final PaymentTransactionJson json,
+ @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List<String> paymentControlPluginNames,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
@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 PaymentApiException, AccountApiException {
- return voidPaymentInternal(json, null, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+ return voidPaymentInternal(json, null, paymentControlPluginNames, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
}
private Response voidPaymentInternal(final PaymentTransactionJson json,
@Nullable final String paymentIdStr,
+ final List<String> paymentControlPluginNames,
final List<String> pluginPropertiesString,
final String createdBy,
final String reason,
@@ -544,7 +557,10 @@ public class PaymentResource extends ComboPaymentResource {
final Account account = accountUserApi.getAccountById(initialPayment.getAccountId(), callContext);
final String transactionExternalKey = json != null ? json.getTransactionExternalKey() : null;
- final Payment payment = paymentApi.createVoid(account, initialPayment.getId(), transactionExternalKey, pluginProperties, callContext);
+ final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
+
+ final Payment payment = paymentApi.createVoidWithPaymentControl(account, initialPayment.getId(), transactionExternalKey,
+ pluginProperties, paymentOptions, callContext);
return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", payment.getId());
}
@@ -558,13 +574,14 @@ public class PaymentResource extends ComboPaymentResource {
@ApiResponse(code = 404, message = "Account not found")})
public Response chargebackPayment(final PaymentTransactionJson json,
@PathParam("paymentId") final String paymentIdStr,
+ @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List<String> paymentControlPluginNames,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
@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 PaymentApiException, AccountApiException {
- return chargebackPaymentInternal(json, paymentIdStr, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+ return chargebackPaymentInternal(json, paymentIdStr, paymentControlPluginNames, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
}
@TimedResource(name = "chargebackPayment")
@@ -575,17 +592,19 @@ public class PaymentResource extends ComboPaymentResource {
@ApiOperation(value = "Record a chargeback")
@ApiResponses(value = {@ApiResponse(code = 404, message = "Account not found")})
public Response chargebackPaymentByExternalKey(final PaymentTransactionJson json,
+ @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List<String> paymentControlPluginNames,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
@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 PaymentApiException, AccountApiException {
- return chargebackPaymentInternal(json, null, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+ return chargebackPaymentInternal(json, null, paymentControlPluginNames, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
}
private Response chargebackPaymentInternal(final PaymentTransactionJson json,
@Nullable final String paymentIdStr,
+ final List<String> paymentControlPluginNames,
final List<String> pluginPropertiesString,
final String createdBy,
final String reason,
@@ -602,8 +621,10 @@ public class PaymentResource extends ComboPaymentResource {
final Account account = accountUserApi.getAccountById(initialPayment.getAccountId(), callContext);
final Currency currency = json.getCurrency() == null ? account.getCurrency() : Currency.valueOf(json.getCurrency());
- final Payment payment = paymentApi.createChargeback(account, initialPayment.getId(), json.getAmount(), currency,
- json.getTransactionExternalKey(), callContext);
+ final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
+
+ final Payment payment = paymentApi.createChargebackWithPaymentControl(account, initialPayment.getId(), json.getAmount(), currency,
+ json.getTransactionExternalKey(), paymentOptions, callContext);
return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", payment.getId());
}
diff --git a/junction/src/test/java/org/killbill/billing/junction/TestDefaultBlockingState.java b/junction/src/test/java/org/killbill/billing/junction/TestDefaultBlockingState.java
index 2bc9e33..08616d2 100644
--- a/junction/src/test/java/org/killbill/billing/junction/TestDefaultBlockingState.java
+++ b/junction/src/test/java/org/killbill/billing/junction/TestDefaultBlockingState.java
@@ -45,7 +45,7 @@ public class TestDefaultBlockingState extends JunctionTestSuiteNoDB {
final BlockingState bs2 = new DefaultBlockingState(UUID.randomUUID(),
UUID.randomUUID(),
BlockingStateType.ACCOUNT,
- DefaultBlockingState.CLEAR_STATE_NAME,
+ "OD2",
"test",
false,
false,
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueInternalApi.java b/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueInternalApi.java
index 81c4002..a2701e4 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueInternalApi.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueInternalApi.java
@@ -26,6 +26,7 @@ import org.killbill.billing.ErrorCode;
import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.junction.BlockingInternalApi;
import org.killbill.billing.overdue.OverdueInternalApi;
@@ -50,6 +51,7 @@ import org.killbill.clock.Clock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.base.MoreObjects;
import com.google.inject.Inject;
public class DefaultOverdueInternalApi implements OverdueInternalApi {
@@ -83,7 +85,8 @@ public class DefaultOverdueInternalApi implements OverdueInternalApi {
public OverdueState getOverdueStateFor(final ImmutableAccountData overdueable, final TenantContext context) throws OverdueException {
try {
final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(context);
- final String stateName = accessApi.getBlockingStateForService(overdueable.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContextFactory.createInternalTenantContext(context)).getStateName();
+ final BlockingState blockingStateForService = accessApi.getBlockingStateForService(overdueable.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContextFactory.createInternalTenantContext(context));
+ final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
final OverdueConfig overdueConfig = overdueConfigCache.getOverdueConfig(internalTenantContext);
final OverdueStateSet states = ((DefaultOverdueConfig) overdueConfig).getOverdueStatesAccount();
return states.findState(stateName);
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStateSet.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStateSet.java
index 0628bae..eb71506 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStateSet.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStateSet.java
@@ -21,27 +21,26 @@ import javax.xml.bind.annotation.XmlAccessorType;
import org.joda.time.LocalDate;
import org.joda.time.Period;
-
import org.killbill.billing.ErrorCode;
import org.killbill.billing.overdue.api.OverdueApiException;
import org.killbill.billing.overdue.api.OverdueState;
import org.killbill.billing.overdue.config.api.BillingState;
import org.killbill.billing.overdue.config.api.OverdueStateSet;
+import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.killbill.xmlloader.ValidatingConfig;
import org.killbill.xmlloader.ValidationErrors;
-import org.killbill.billing.junction.DefaultBlockingState;
@XmlAccessorType(XmlAccessType.NONE)
public abstract class DefaultOverdueStateSet extends ValidatingConfig<DefaultOverdueConfig> implements OverdueStateSet {
private static final Period ZERO_PERIOD = new Period();
- private final DefaultOverdueState clearState = new DefaultOverdueState().setName(DefaultBlockingState.CLEAR_STATE_NAME).setClearState(true);
+ private final DefaultOverdueState clearState = new DefaultOverdueState().setName(OverdueWrapper.CLEAR_STATE_NAME).setClearState(true);
public abstract DefaultOverdueState[] getStates();
@Override
public OverdueState findState(final String stateName) throws OverdueApiException {
- if (stateName.equals(DefaultBlockingState.CLEAR_STATE_NAME)) {
+ if (stateName.equals(OverdueWrapper.CLEAR_STATE_NAME)) {
return clearState;
}
for (final DefaultOverdueState state : getStates()) {
@@ -52,7 +51,6 @@ public abstract class DefaultOverdueStateSet extends ValidatingConfig<DefaultOve
throw new OverdueApiException(ErrorCode.CAT_NO_SUCH_OVEDUE_STATE, stateName);
}
-
/* (non-Javadoc)
* @see org.killbill.billing.catalog.overdue.OverdueBillingState#findClearState()
*/
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java b/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java
index 8f1515f..47b4071 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java
@@ -28,9 +28,12 @@ import org.killbill.billing.events.InvoiceAdjustmentInternalEvent;
import org.killbill.billing.events.InvoicePaymentErrorInternalEvent;
import org.killbill.billing.events.InvoicePaymentInfoInternalEvent;
import org.killbill.billing.overdue.OverdueInternalApi;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.callcontext.CallOrigin;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.billing.util.dao.NonEntityDao;
import org.killbill.billing.util.tag.ControlTagType;
import org.killbill.bus.api.BusEvent;
import org.slf4j.Logger;
@@ -46,28 +49,45 @@ public class OverdueListener {
private final OverdueInternalApi overdueInternalApi;
private final InternalCallContextFactory internalCallContextFactory;
+ private final CacheControllerDispatcher cacheControllerDispatcher;
+
+ private final NonEntityDao nonEntityDao;
@Inject
public OverdueListener(final OverdueInternalApi overdueInternalApi,
+ final NonEntityDao nonEntityDao,
+ final CacheControllerDispatcher cacheControllerDispatcher,
final InternalCallContextFactory internalCallContextFactory) {
this.overdueInternalApi = overdueInternalApi;
+ this.nonEntityDao = nonEntityDao;
+ this.cacheControllerDispatcher = cacheControllerDispatcher;
this.internalCallContextFactory = internalCallContextFactory;
}
+
+
+
@AllowConcurrentEvents
@Subscribe
- public void handle_OVERDUE_ENFORCEMENT_OFF_Insert(final ControlTagCreationInternalEvent event) {
+ public void handleTagInsert(final ControlTagCreationInternalEvent event) {
if (event.getTagDefinition().getName().equals(ControlTagType.OVERDUE_ENFORCEMENT_OFF.toString()) && event.getObjectType() == ObjectType.ACCOUNT) {
final InternalCallContext callContext = createCallContext(event.getUserToken(), event.getSearchKey1(), event.getSearchKey2());
overdueInternalApi.scheduleOverdueClear(event.getObjectId(), callContext);
+ } else if (event.getTagDefinition().getName().equals(ControlTagType.WRITTEN_OFF.toString()) && event.getObjectType() == ObjectType.INVOICE) {
+ final UUID accountId = nonEntityDao.retrieveIdFromObject(event.getSearchKey1(), ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID));
+ insertBusEventIntoNotificationQueue(accountId, event);
}
}
+
@AllowConcurrentEvents
@Subscribe
- public void handle_OVERDUE_ENFORCEMENT_OFF_Removal(final ControlTagDeletionInternalEvent event) {
+ public void handleTagRemoval(final ControlTagDeletionInternalEvent event) {
if (event.getTagDefinition().getName().equals(ControlTagType.OVERDUE_ENFORCEMENT_OFF.toString()) && event.getObjectType() == ObjectType.ACCOUNT) {
insertBusEventIntoNotificationQueue(event.getObjectId(), event);
+ } else if (event.getTagDefinition().getName().equals(ControlTagType.WRITTEN_OFF.toString()) && event.getObjectType() == ObjectType.INVOICE) {
+ final UUID accountId = nonEntityDao.retrieveIdFromObject(event.getSearchKey1(), ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID));
+ insertBusEventIntoNotificationQueue(accountId, event);
}
}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java
index a7c14f8..95a2b7d 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java
@@ -21,6 +21,7 @@ package org.killbill.billing.overdue.wrapper;
import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.junction.BlockingInternalApi;
import org.killbill.billing.overdue.OverdueService;
@@ -39,8 +40,13 @@ import org.killbill.commons.locker.LockFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.base.MoreObjects;
+
public class OverdueWrapper {
+
+ public static final String CLEAR_STATE_NAME = "__KILLBILL__CLEAR__OVERDUE_STATE__";
+
private static final Logger log = LoggerFactory.getLogger(OverdueWrapper.class);
// Should we introduce a config?
@@ -93,8 +99,8 @@ public class OverdueWrapper {
private OverdueState refreshWithLock(final InternalCallContext context) throws OverdueException, OverdueApiException {
final BillingState billingState = billingState(context);
- final String previousOverdueStateName = api.getBlockingStateForService(overdueable.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, context).getStateName();
-
+ final BlockingState blockingStateForService = api.getBlockingStateForService(overdueable.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, context);
+ final String previousOverdueStateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
final OverdueState currentOverdueState = overdueStateSet.findState(previousOverdueStateName);
final OverdueState nextOverdueState = overdueStateSet.calculateOverdueState(billingState, clock.getToday(billingState.getAccountTimeZone()));
@@ -120,7 +126,8 @@ public class OverdueWrapper {
}
private void clearWithLock(final InternalCallContext context) throws OverdueException, OverdueApiException {
- final String previousOverdueStateName = api.getBlockingStateForService(overdueable.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, context).getStateName();
+ final BlockingState blockingStateForService = api.getBlockingStateForService(overdueable.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, context);
+ final String previousOverdueStateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
final OverdueState previousOverdueState = overdueStateSet.findState(previousOverdueStateName);
overdueStateApplicator.clear(overdueable, previousOverdueState, overdueStateSet.getClearState(), context);
}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java b/overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java
index 5c77f5c..2222377 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java
@@ -25,6 +25,7 @@ import java.util.concurrent.Callable;
import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.overdue.api.OverdueState;
import org.killbill.billing.overdue.config.DefaultOverdueConfig;
+import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -49,7 +50,7 @@ public class TestOverdueStateApplicator extends OverdueTestSuiteWithEmbeddedDB {
Mockito.when(account.getId()).thenReturn(UUID.randomUUID());
final OverdueStateSet overdueStateSet = config.getOverdueStatesAccount();
- final OverdueState clearState = config.getOverdueStatesAccount().findState(DefaultBlockingState.CLEAR_STATE_NAME);
+ final OverdueState clearState = config.getOverdueStatesAccount().findState(OverdueWrapper.CLEAR_STATE_NAME);
OverdueState state;
state = config.getOverdueStatesAccount().findState("OD1");
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java b/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java
index 142f39d..df5a469 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java
@@ -21,6 +21,7 @@ package org.killbill.billing.overdue.glue;
import java.util.List;
import java.util.UUID;
+import org.joda.time.DateTime;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.entitlement.api.BlockingState;
@@ -37,6 +38,7 @@ import org.killbill.billing.overdue.applicator.OverdueBusListenerTester;
import org.killbill.billing.overdue.caching.MockOverdueConfigCache;
import org.killbill.billing.overdue.caching.OverdueCacheInvalidationCallback;
import org.killbill.billing.overdue.caching.OverdueConfigCache;
+import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.killbill.billing.platform.api.KillbillConfigSource;
import org.killbill.billing.tenant.api.TenantInternalApi.CacheInvalidationCallback;
import org.killbill.billing.util.email.EmailModule;
@@ -46,6 +48,7 @@ import org.killbill.billing.util.glue.CacheModule;
import org.killbill.billing.util.glue.CallContextModule;
import org.killbill.billing.util.glue.CustomFieldModule;
import org.killbill.billing.util.glue.MemoryGlobalLockerModule;
+import org.killbill.clock.Clock;
import org.killbill.clock.ClockMock;
import com.google.inject.name.Names;
@@ -90,12 +93,13 @@ public class TestOverdueModule extends DefaultOverdueModule {
return blockingState;
}
+
@Override
public BlockingState getBlockingStateForService(final UUID blockableId, final BlockingStateType blockingStateType, final String serviceName, final InternalTenantContext context) {
if (blockingState != null && blockingState.getBlockedId().equals(blockableId)) {
return blockingState;
} else {
- return DefaultBlockingState.getClearState(blockingStateType, serviceName, new ClockMock());
+ return new DefaultBlockingState(null, blockingStateType, OverdueWrapper.CLEAR_STATE_NAME, serviceName, false, false, false, null);
}
}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/wrapper/TestOverdueWrapper.java b/overdue/src/test/java/org/killbill/billing/overdue/wrapper/TestOverdueWrapper.java
index c6e2231..2c22056 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/wrapper/TestOverdueWrapper.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/wrapper/TestOverdueWrapper.java
@@ -76,7 +76,7 @@ public class TestOverdueWrapper extends OverdueTestSuiteWithEmbeddedDB {
final InputStream is = new ByteArrayInputStream(testOverdueHelper.getConfigXml().getBytes());
final DefaultOverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, DefaultOverdueConfig.class);
- state = config.getOverdueStatesAccount().findState(DefaultBlockingState.CLEAR_STATE_NAME);
+ state = config.getOverdueStatesAccount().findState(OverdueWrapper.CLEAR_STATE_NAME);
account = testOverdueHelper.createImmutableAccountData(clock.getUTCToday().minusDays(31));
wrapper = overdueWrapperFactory.createOverdueWrapperFor(account, internalCallContext);
final OverdueState result = wrapper.refresh(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 f6c86e6..14e7ab0 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
@@ -72,21 +72,26 @@ public class DefaultPayment extends EntityBase implements Payment {
private static BigDecimal getAmountForType(final Iterable<PaymentTransaction> transactions, final TransactionType transactiontype) {
BigDecimal result = BigDecimal.ZERO;
- final Iterable<PaymentTransaction> filtered = Iterables.filter(transactions, new Predicate<PaymentTransaction>() {
- @Override
- public boolean apply(final PaymentTransaction input) {
- return input.getTransactionType() == transactiontype && TransactionStatus.SUCCESS.equals(input.getTransactionStatus());
+ BigDecimal processedResult = BigDecimal.ZERO;
+ boolean shouldUseProcessedAmount = true;
+
+ for (final PaymentTransaction transaction : transactions) {
+ if (transaction.getTransactionType() != transactiontype || !TransactionStatus.SUCCESS.equals(transaction.getTransactionStatus())) {
+ continue;
}
- });
- if (TransactionType.AUTHORIZE.equals(transactiontype) && filtered.iterator().hasNext()) {
- // HACK - For multi-step AUTH, don't sum the individual transactions
- result = filtered.iterator().next().getAmount();
- } else {
- for (final PaymentTransaction dpt : filtered) {
- result = result.add(dpt.getAmount());
+
+ result = result.add(transaction.getAmount());
+
+ shouldUseProcessedAmount = shouldUseProcessedAmount && transaction.getCurrency().equals(transaction.getProcessedCurrency()) && transaction.getProcessedAmount() != null;
+ processedResult = shouldUseProcessedAmount ? processedResult.add(transaction.getProcessedAmount()) : BigDecimal.ZERO;
+
+ // For multi-step AUTH, don't sum the individual transactions
+ if (TransactionType.AUTHORIZE.equals(transactiontype)) {
+ break;
}
}
- return result;
+
+ return shouldUseProcessedAmount ? processedResult : result;
}
@Override
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 15dc191..c71407d 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
@@ -211,11 +211,23 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
// Recompute new lastSuccessPaymentState. This is important to be able to allow new operations on the state machine (for e.g an AUTH_SUCCESS would now allow a CAPTURE operation)
final String lastSuccessPaymentState = paymentStateMachineHelper.isSuccessState(newPaymentState) ? newPaymentState : null;
- // Update the processedAmount, processedCurrency if we got a paymentTransactionInfoPlugin from the plugin and if this is a non error state
- final BigDecimal processedAmount = (paymentTransactionInfoPlugin != null && isPendingOrFinalTransactionStatus(transactionStatus)) ?
- paymentTransactionInfoPlugin.getAmount() : paymentTransaction.getProcessedAmount();
- final Currency processedCurrency = (paymentTransactionInfoPlugin != null && isPendingOrFinalTransactionStatus(transactionStatus)) ?
- paymentTransactionInfoPlugin.getCurrency() : paymentTransaction.getProcessedCurrency();
+ // Update processedAmount and processedCurrency
+ final BigDecimal processedAmount;
+ if (TransactionStatus.SUCCESS.equals(transactionStatus) || TransactionStatus.PENDING.equals(transactionStatus)) {
+ if (paymentTransactionInfoPlugin == null || paymentTransactionInfoPlugin.getAmount() == null) {
+ processedAmount = paymentTransaction.getProcessedAmount();
+ } else {
+ processedAmount = paymentTransactionInfoPlugin.getAmount();
+ }
+ } else {
+ processedAmount = BigDecimal.ZERO;
+ }
+ final Currency processedCurrency;
+ if (paymentTransactionInfoPlugin == null || paymentTransactionInfoPlugin.getCurrency() == null) {
+ processedCurrency = paymentTransaction.getProcessedCurrency();
+ } else {
+ processedCurrency = paymentTransactionInfoPlugin.getCurrency();
+ }
// Update the gatewayErrorCode, gatewayError if we got a paymentTransactionInfoPlugin
final String gatewayErrorCode = paymentTransactionInfoPlugin != null ? paymentTransactionInfoPlugin.getGatewayErrorCode() : paymentTransaction.getGatewayErrorCode();
@@ -239,12 +251,6 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
return (newTransactionStatus != TransactionStatus.UNKNOWN) ? newTransactionStatus : currentTransactionStatus;
}
- private boolean isPendingOrFinalTransactionStatus(final TransactionStatus transactionStatus) {
- return (transactionStatus == TransactionStatus.PENDING ||
- transactionStatus == TransactionStatus.SUCCESS ||
- transactionStatus == TransactionStatus.PAYMENT_FAILURE);
- }
-
private PaymentPluginApi getPaymentPluginApi(final PaymentModelDao item, final String pluginName) {
final PaymentPluginApi pluginApi = pluginRegistry.getServiceForName(pluginName);
Preconditions.checkState(pluginApi != null, "Janitor IncompletePaymentTransactionTask cannot retrieve PaymentPluginApi for plugin %s (payment id %s), skipping", pluginName, item.getId());
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 2b3a215..c6545dc 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
@@ -30,6 +30,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.TransactionStatus;
+import org.killbill.billing.payment.api.TransactionType;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.dao.PaymentMethodModelDao;
import org.killbill.billing.payment.dao.PaymentModelDao;
@@ -89,7 +90,10 @@ public class PaymentAutomatonDAOHelper {
if (existingTransactions.isEmpty()) {
throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_SUCCESS_PAYMENT, paymentStateContext.getPaymentId());
}
- if (paymentStateContext.getCurrency() != null && existingTransactions.get(0).getCurrency() != paymentStateContext.getCurrency()) {
+ if (paymentStateContext.getCurrency() != null &&
+ existingTransactions.get(0).getCurrency() != paymentStateContext.getCurrency() &&
+ !TransactionType.CHARGEBACK.equals(paymentStateContext.getTransactionType())) {
+ // Note that we allow chargebacks in a different currency
throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, "currency", " should be " + existingTransactions.get(0).getCurrency() + " to match other existing transactions");
}
@@ -103,8 +107,22 @@ public class PaymentAutomatonDAOHelper {
public void processPaymentInfoPlugin(final TransactionStatus transactionStatus, @Nullable final PaymentTransactionInfoPlugin paymentInfoPlugin,
final String currentPaymentStateName) {
- final BigDecimal processedAmount = paymentInfoPlugin == null ? null : paymentInfoPlugin.getAmount();
- final Currency processedCurrency = paymentInfoPlugin == null ? null : paymentInfoPlugin.getCurrency();
+ final BigDecimal processedAmount;
+ if (TransactionStatus.SUCCESS.equals(transactionStatus) || TransactionStatus.PENDING.equals(transactionStatus)) {
+ if (paymentInfoPlugin == null || paymentInfoPlugin.getAmount() == null) {
+ processedAmount = paymentStateContext.getAmount();
+ } else {
+ processedAmount = paymentInfoPlugin.getAmount();
+ }
+ } else {
+ processedAmount = BigDecimal.ZERO;
+ }
+ final Currency processedCurrency;
+ if (paymentInfoPlugin == null || paymentInfoPlugin.getCurrency() == null) {
+ processedCurrency = paymentStateContext.getCurrency();
+ } else {
+ processedCurrency = paymentInfoPlugin.getCurrency();
+ }
final String gatewayErrorCode = paymentInfoPlugin == null ? null : paymentInfoPlugin.getGatewayErrorCode();
final String gatewayErrorMsg = paymentInfoPlugin == null ? null : paymentInfoPlugin.getGatewayError();
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackOperation.java
index a2993a6..cf477d5 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackOperation.java
@@ -17,14 +17,11 @@
package org.killbill.billing.payment.core.sm.payments;
-import java.math.BigDecimal;
-
import org.killbill.automaton.OperationResult;
import org.killbill.billing.payment.api.PaymentApiException;
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.killbill.billing.payment.dispatcher.PluginDispatcher;
import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
@@ -50,28 +47,6 @@ public class ChargebackOperation extends PaymentOperation {
@Override
protected PaymentTransactionInfoPlugin doCallSpecificOperationCallback() throws PaymentPluginApiException {
logger.debug("Starting CHARGEBACK for payment {} ({} {})", paymentStateContext.getPaymentId(), paymentStateContext.getAmount(), paymentStateContext.getCurrency());
-
- final PaymentPluginStatus status;
- if (!paymentStateContext.getOnLeavingStateExistingTransactions().isEmpty()) {
- final Iterable<PaymentTransactionModelDao> purchaseTransactions = getOnLeavingStateExistingTransactionsForType(TransactionType.PURCHASE);
- final Iterable<PaymentTransactionModelDao> captureTransactions = getOnLeavingStateExistingTransactionsForType(TransactionType.CAPTURE);
- final Iterable<PaymentTransactionModelDao> refundTransactions = getOnLeavingStateExistingTransactionsForType(TransactionType.REFUND);
- final Iterable<PaymentTransactionModelDao> chargebackTransactions = getOnLeavingStateExistingTransactionsForType(TransactionType.CHARGEBACK);
-
- final BigDecimal purchasedAmount = getSumAmount(purchaseTransactions);
- final BigDecimal capturedAmount = getSumAmount(captureTransactions);
- final BigDecimal refundedAmount = getSumAmount(refundTransactions);
- final BigDecimal chargebackAmount = getSumAmount(chargebackTransactions);
- final BigDecimal chargebackAvailableAmount = purchasedAmount.add(capturedAmount).subtract(refundedAmount.add(chargebackAmount));
-
- if (paymentStateContext.getAmount().compareTo(chargebackAvailableAmount) > 0) {
- status = PaymentPluginStatus.ERROR;
- } else {
- status = PaymentPluginStatus.PROCESSED;
- }
- } else {
- status = PaymentPluginStatus.PROCESSED;
- }
return new DefaultNoOpPaymentInfoPlugin(paymentStateContext.getPaymentId(),
paymentStateContext.getTransactionId(),
TransactionType.CHARGEBACK,
@@ -79,7 +54,7 @@ public class ChargebackOperation extends PaymentOperation {
paymentStateContext.getCurrency(),
null,
null,
- status,
+ PaymentPluginStatus.PROCESSED,
null,
null);
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentModelDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentModelDao.java
index 612fed7..b15be00 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentModelDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentModelDao.java
@@ -1,7 +1,8 @@
/*
- * Copyright 2014 Groupon, Inc
+ * 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 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:
*
@@ -103,6 +104,21 @@ public class PaymentModelDao extends EntityModelDaoBase implements EntityModelDa
}
@Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("PaymentModelDao{");
+ sb.append("accountId=").append(accountId);
+ sb.append(", paymentNumber=").append(paymentNumber);
+ sb.append(", paymentMethodId=").append(paymentMethodId);
+ sb.append(", externalKey='").append(externalKey).append('\'');
+ sb.append(", stateName='").append(stateName).append('\'');
+ sb.append(", lastSuccessStateName='").append(lastSuccessStateName).append('\'');
+ sb.append(", createdDate=").append(createdDate);
+ sb.append(", updatedDate=").append(updatedDate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
public boolean equals(final Object o) {
if (this == o) {
return true;
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentTransactionModelDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentTransactionModelDao.java
index ac0f0da..3f11cf9 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentTransactionModelDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentTransactionModelDao.java
@@ -1,7 +1,8 @@
/*
- * Copyright 2014 Groupon, Inc
+ * 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 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:
*
@@ -225,6 +226,27 @@ public class PaymentTransactionModelDao extends EntityModelDaoBase implements En
}
@Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("PaymentTransactionModelDao{");
+ sb.append("attemptId=").append(attemptId);
+ sb.append(", paymentId=").append(paymentId);
+ sb.append(", transactionExternalKey='").append(transactionExternalKey).append('\'');
+ sb.append(", transactionType=").append(transactionType);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append(", transactionStatus=").append(transactionStatus);
+ sb.append(", amount=").append(amount);
+ sb.append(", currency=").append(currency);
+ sb.append(", processedAmount=").append(processedAmount);
+ sb.append(", processedCurrency=").append(processedCurrency);
+ sb.append(", gatewayErrorCode='").append(gatewayErrorCode).append('\'');
+ sb.append(", gatewayErrorMsg='").append(gatewayErrorMsg).append('\'');
+ sb.append(", createdDate=").append(createdDate);
+ sb.append(", updatedDate=").append(updatedDate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (paymentId != null ? paymentId.hashCode() : 0);
diff --git a/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java b/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
index b5be8c5..d8012d2 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.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,23 +17,27 @@
package org.killbill.billing.payment.dispatcher;
+import java.util.Random;
import java.util.concurrent.Callable;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
+import org.killbill.billing.util.UUIDs;
import org.killbill.commons.request.Request;
import org.killbill.commons.request.RequestData;
public class CallableWithRequestData<T> implements Callable<T> {
private final RequestData requestData;
+ private final Random random;
private final SecurityManager securityManager;
private final Subject subject;
private final Callable<T> delegate;
- public CallableWithRequestData(final RequestData requestData, final SecurityManager securityManager, final Subject subject, final Callable<T> delegate) {
+ public CallableWithRequestData(final RequestData requestData, final Random random, final SecurityManager securityManager, final Subject subject, final Callable<T> delegate) {
this.requestData = requestData;
+ this.random = random;
this.securityManager = securityManager;
this.subject = subject;
this.delegate = delegate;
@@ -43,11 +47,13 @@ public class CallableWithRequestData<T> implements Callable<T> {
public T call() throws Exception {
try {
Request.setPerThreadRequestData(requestData);
+ UUIDs.setRandom(random);
ThreadContext.bind(securityManager);
ThreadContext.bind(subject);
return delegate.call();
} finally {
Request.resetPerThreadRequestData();
+ UUIDs.setRandom(null);
ThreadContext.unbindSecurityManager();
ThreadContext.unbindSubject();
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java b/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java
index a457e17..fa65e1b 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.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
@@ -27,6 +27,7 @@ import java.util.concurrent.TimeoutException;
import org.apache.shiro.util.ThreadContext;
import org.killbill.billing.payment.core.PaymentExecutors;
+import org.killbill.billing.util.UUIDs;
import org.killbill.commons.profiling.Profiling;
import org.killbill.commons.profiling.ProfilingData;
import org.killbill.commons.request.Request;
@@ -57,7 +58,11 @@ public class PluginDispatcher<ReturnType> {
final ExecutorService pluginExecutor = paymentExecutors.getPluginExecutorService();
// Wrap existing callable to keep the original requestId
- final Callable<PluginDispatcherReturnType<ReturnType>> callableWithRequestData = new CallableWithRequestData(Request.getPerThreadRequestData(), ThreadContext.getSecurityManager(), ThreadContext.getSubject(), task);
+ final Callable<PluginDispatcherReturnType<ReturnType>> callableWithRequestData = new CallableWithRequestData(Request.getPerThreadRequestData(),
+ UUIDs.getRandom(),
+ ThreadContext.getSecurityManager(),
+ ThreadContext.getSubject(),
+ task);
final Future<PluginDispatcherReturnType<ReturnType>> future = pluginExecutor.submit(callableWithRequestData);
final PluginDispatcherReturnType<ReturnType> pluginDispatcherResult = future.get(timeout, unit);
diff --git a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
index 86b22f2..7ccaba6 100644
--- a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
@@ -33,6 +33,7 @@ import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.control.plugin.api.OnFailurePaymentControlResult;
import org.killbill.billing.control.plugin.api.OnSuccessPaymentControlResult;
import org.killbill.billing.control.plugin.api.PaymentApiType;
@@ -152,11 +153,19 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
final UUID invoiceId = getInvoiceId(pluginProperties);
existingInvoicePayment = invoiceApi.getInvoicePaymentForAttempt(paymentControlContext.getPaymentId(), internalContext);
if (existingInvoicePayment != null && existingInvoicePayment.isSuccess()) {
+ // Only one successful purchase per payment (the invoice could be linked to multiple successful payments though)
log.info("onSuccessCall was already completed for payment purchase: " + paymentControlContext.getPaymentId());
} else {
- log.debug("Notifying invoice of successful payment: id={}, amount={}, currency={}, invoiceId={}", paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), paymentControlContext.getCurrency(), invoiceId);
+ final BigDecimal invoicePaymentAmount;
+ if (paymentControlContext.getCurrency() == paymentControlContext.getProcessedCurrency()) {
+ invoicePaymentAmount = paymentControlContext.getProcessedAmount();
+ } else {
+ log.warn("Currency {} of invoice payment {} doesn't match invoice currency {}, assuming it is a full payment" , paymentControlContext.getProcessedCurrency(), paymentControlContext.getPaymentId(), paymentControlContext.getCurrency());
+ invoicePaymentAmount = paymentControlContext.getAmount();
+ }
+ log.debug("Notifying invoice of successful payment: id={}, amount={}, currency={}, invoiceId={}", paymentControlContext.getPaymentId(), invoicePaymentAmount, paymentControlContext.getCurrency(), invoiceId);
invoiceApi.notifyOfPayment(invoiceId,
- paymentControlContext.getAmount(),
+ invoicePaymentAmount,
paymentControlContext.getCurrency(),
paymentControlContext.getProcessedCurrency(),
paymentControlContext.getPaymentId(),
@@ -167,23 +176,34 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
break;
case REFUND:
- existingInvoicePayment = invoiceApi.getInvoicePaymentForRefund(paymentControlContext.getPaymentId(), internalContext);
- if (existingInvoicePayment != null) {
- log.info("onSuccessCall was already completed for payment refund: " + paymentControlContext.getPaymentId());
- } else {
- final Map<UUID, BigDecimal> idWithAmount = extractIdsWithAmountFromProperties(pluginProperties);
- final PluginProperty prop = getPluginProperty(pluginProperties, PROP_IPCD_REFUND_WITH_ADJUSTMENTS);
- final boolean isAdjusted = prop != null ? Boolean.valueOf((String) prop.getValue()) : false;
- invoiceApi.createRefund(paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), isAdjusted, idWithAmount, paymentControlContext.getTransactionExternalKey(), internalContext);
- }
+ final Map<UUID, BigDecimal> idWithAmount = extractIdsWithAmountFromProperties(pluginProperties);
+ final PluginProperty prop = getPluginProperty(pluginProperties, PROP_IPCD_REFUND_WITH_ADJUSTMENTS);
+ final boolean isAdjusted = prop != null ? Boolean.valueOf((String) prop.getValue()) : false;
+ invoiceApi.createRefund(paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), isAdjusted, idWithAmount, paymentControlContext.getTransactionExternalKey(), internalContext);
break;
case CHARGEBACK:
existingInvoicePayment = invoiceApi.getInvoicePaymentForChargeback(paymentControlContext.getPaymentId(), internalContext);
if (existingInvoicePayment != null) {
+ // We don't support partial chargebacks (yet?)
log.info("onSuccessCall was already completed for payment chargeback: " + paymentControlContext.getPaymentId());
} else {
- invoiceApi.createChargeback(paymentControlContext.getPaymentId(), paymentControlContext.getProcessedAmount(), paymentControlContext.getProcessedCurrency(), internalContext);
+ final InvoicePayment linkedInvoicePayment = invoiceApi.getInvoicePaymentForAttempt(paymentControlContext.getPaymentId(), internalContext);
+
+ final BigDecimal amount;
+ final Currency currency;
+ if (linkedInvoicePayment.getCurrency().equals(paymentControlContext.getProcessedCurrency()) && paymentControlContext.getProcessedAmount() != null) {
+ amount = paymentControlContext.getProcessedAmount();
+ currency = paymentControlContext.getProcessedCurrency();
+ } else if (linkedInvoicePayment.getCurrency().equals(paymentControlContext.getCurrency()) && paymentControlContext.getAmount() != null) {
+ amount = paymentControlContext.getAmount();
+ currency = paymentControlContext.getCurrency();
+ } else {
+ amount = linkedInvoicePayment.getAmount();
+ currency = linkedInvoicePayment.getCurrency();
+ }
+
+ invoiceApi.createChargeback(paymentControlContext.getPaymentId(), amount, currency, internalContext);
}
break;
@@ -209,7 +229,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
try {
log.debug("Notifying invoice of failed payment: id={}, amount={}, currency={}, invoiceId={}", paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), paymentControlContext.getCurrency(), invoiceId);
invoiceApi.notifyOfPayment(invoiceId,
- paymentControlContext.getAmount(),
+ BigDecimal.ZERO,
paymentControlContext.getCurrency(),
// processed currency may be null so we use currency; processed currency will be updated if/when payment succeeds
paymentControlContext.getCurrency(),
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 d5c1aa5..d6e293e 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
@@ -182,7 +182,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(payment.getTransactions().get(0).getPaymentId(), payment.getId());
assertEquals(payment.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0);
assertEquals(payment.getTransactions().get(0).getCurrency(), Currency.AED);
- assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0);
+ assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(BigDecimal.ZERO), 0);
assertEquals(payment.getTransactions().get(0).getProcessedCurrency(), Currency.AED);
assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
@@ -405,7 +405,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(payment.getTransactions().get(0).getPaymentId(), payment.getId());
assertEquals(payment.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0);
assertEquals(payment.getTransactions().get(0).getCurrency(), Currency.USD);
- assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0); // This is weird...
+ assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(BigDecimal.ZERO), 0);
assertEquals(payment.getTransactions().get(0).getProcessedCurrency(), Currency.USD);
assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
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 650bd00..41155e8 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
@@ -1,8 +1,8 @@
/*
- * 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 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:
*
@@ -21,6 +21,7 @@ import java.math.BigDecimal;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -45,8 +46,6 @@ import org.killbill.billing.util.config.PaymentConfig;
import org.killbill.commons.locker.GlobalLocker;
import org.killbill.commons.locker.memory.MemoryGlobalLocker;
import org.mockito.Mockito;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -56,13 +55,13 @@ import com.jayway.awaitility.Awaitility;
public class TestPluginOperation extends PaymentTestSuiteNoDB {
- private final static String PLUGIN_NAME_PLACEHOLDER = "pluginName";
+ private static final String PLUGIN_NAME_PLACEHOLDER = "pluginName";
+
+ private static final int TIMEOUT = 5;
private final GlobalLocker locker = new MemoryGlobalLocker();
private final Account account = Mockito.mock(Account.class);
- private static final Logger logger = LoggerFactory.getLogger(TestPluginOperation.class);
-
@BeforeMethod(groups = "fast")
public void beforeMethod() throws Exception {
super.beforeMethod();
@@ -70,11 +69,10 @@ public class TestPluginOperation extends PaymentTestSuiteNoDB {
}
@Test(groups = "fast")
- public void testWithAccountLock() throws Exception {
- testLocking(true);
+ public void testAccountLock() throws Exception {
+ testLocking();
}
-
@Test(groups = "fast")
public void testOperationThrowsPaymentApiException() throws Exception {
final CallbackTest callback = new CallbackTest(new PaymentApiException(ErrorCode.__UNKNOWN_ERROR_CODE));
@@ -103,30 +101,31 @@ public class TestPluginOperation extends PaymentTestSuiteNoDB {
}
}
- private void testLocking(final boolean withAccountLock) throws Exception {
+ private void testLocking() throws Exception {
final Semaphore available = new Semaphore(1, true);
final CallbackTest callback = new CallbackTest(available);
- final PaymentOperation pluginOperation = getPluginOperation(withAccountLock);
+ final PaymentOperation pluginOperation = getPluginOperation(true);
// Take the only permit
available.acquire();
// Start the plugin operation in the background (will block)
runPluginOperationInBackground(pluginOperation, callback, false);
+ Awaitility.await()
+ .until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return callback.getStartCount() == 1;
+ }
+ });
// The operation should be blocked here because we have the semaphore
- Assert.assertEquals(available.getQueueLength(), 1);
Assert.assertEquals(callback.getRunCount(), 0);
- if (withAccountLock) {
- // If the account is locked, trying to run the operation again will throw LockFailedException
- runPluginOperationInBackground(pluginOperation, callback, true);
- } else {
- // If the account is not locked, it will just block
- runPluginOperationInBackground(pluginOperation, callback, false);
- Assert.assertEquals(available.getQueueLength(), 2);
- Assert.assertEquals(callback.getRunCount(), 0);
- }
+ // Trying to run the operation again will throw LockFailedException
+ Awaitility.await().atMost(2 * TIMEOUT, TimeUnit.SECONDS).untilTrue(runPluginOperationInBackground(pluginOperation, callback, true));
+
+ Assert.assertEquals(callback.getRunCount(), 0);
// Release the semaphore
available.release();
@@ -136,37 +135,41 @@ public class TestPluginOperation extends PaymentTestSuiteNoDB {
.until(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
- return callback.getRunCount() == (withAccountLock ? 1 : 2);
+ return callback.getRunCount() == 1;
}
});
// Verify the final state
- Assert.assertEquals(available.getQueueLength(), 0);
- Assert.assertEquals(callback.getRunCount(), withAccountLock ? 1 : 2);
+ Assert.assertEquals(available.availablePermits(), 1);
}
- private void runPluginOperationInBackground(final PaymentOperation pluginOperation, final CallbackTest callback, final boolean shouldFailBecauseOfLockFailure) throws Exception {
- final AtomicBoolean threadStarted = new AtomicBoolean(false);
+ private AtomicBoolean runPluginOperationInBackground(final PaymentOperation pluginOperation, final CallbackTest callback, final boolean shouldFailBecauseOfLockFailure) throws Exception {
+ final AtomicBoolean threadRunning = new AtomicBoolean(false);
+ final AtomicBoolean threadHasRun = new AtomicBoolean(false);
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
- threadStarted.set(true);
-
- if (shouldFailBecauseOfLockFailure) {
- try {
- pluginOperation.dispatchWithAccountLockAndTimeout(PLUGIN_NAME_PLACEHOLDER, callback);
- Assert.fail();
- } catch (final OperationException e) {
- Assert.assertTrue(e.getCause() instanceof PaymentApiException);
- // No better error code for lock failures...
- Assert.assertEquals(((PaymentApiException) e.getCause()).getCode(), ErrorCode.PAYMENT_INTERNAL_ERROR.getCode());
- }
- } else {
- try {
- pluginOperation.dispatchWithAccountLockAndTimeout(PLUGIN_NAME_PLACEHOLDER, callback);
- } catch (final OperationException e) {
- Assert.fail(e.getMessage());
+ threadRunning.set(true);
+
+ try {
+ if (shouldFailBecauseOfLockFailure) {
+ try {
+ pluginOperation.dispatchWithAccountLockAndTimeout(PLUGIN_NAME_PLACEHOLDER, callback);
+ Assert.fail();
+ } catch (final OperationException e) {
+ Assert.assertTrue(e.getCause() instanceof PaymentApiException);
+ // No better error code for lock failures...
+ Assert.assertEquals(((PaymentApiException) e.getCause()).getCode(), ErrorCode.PAYMENT_INTERNAL_ERROR.getCode());
+ }
+ } else {
+ try {
+ pluginOperation.dispatchWithAccountLockAndTimeout(PLUGIN_NAME_PLACEHOLDER, callback);
+ } catch (final OperationException e) {
+ Assert.fail(e.getMessage());
+ }
}
+ } finally {
+ threadHasRun.set(true);
}
}
});
@@ -174,7 +177,9 @@ public class TestPluginOperation extends PaymentTestSuiteNoDB {
t1.start();
// Make sure the thread has started
- Awaitility.await().untilTrue(threadStarted);
+ Awaitility.await().untilTrue(threadRunning);
+
+ return threadHasRun;
}
private PaymentOperation getPluginOperation() throws PaymentApiException {
@@ -182,7 +187,7 @@ public class TestPluginOperation extends PaymentTestSuiteNoDB {
}
private PaymentOperation getPluginOperation(final boolean shouldLockAccount) throws PaymentApiException {
- return getPluginOperation(shouldLockAccount, Integer.MAX_VALUE);
+ return getPluginOperation(shouldLockAccount, TIMEOUT);
}
private PaymentOperation getPluginOperation(final boolean shouldLockAccount, final int timeoutSeconds) throws PaymentApiException {
@@ -209,6 +214,7 @@ public class TestPluginOperation extends PaymentTestSuiteNoDB {
private static final class CallbackTest implements DispatcherCallback<PluginDispatcherReturnType<OperationResult>, PaymentApiException> {
+ private final AtomicInteger startCount = new AtomicInteger(0);
private final AtomicInteger runCount = new AtomicInteger(0);
private final Semaphore available;
@@ -242,9 +248,11 @@ public class TestPluginOperation extends PaymentTestSuiteNoDB {
@Override
public PluginDispatcherReturnType<OperationResult> doOperation() throws PaymentApiException {
+ startCount.incrementAndGet();
+
try {
if (available != null) {
- available.acquire();
+ available.acquireUninterruptibly();
}
if (sleepTimeMillis != null) {
@@ -272,6 +280,10 @@ public class TestPluginOperation extends PaymentTestSuiteNoDB {
public int getRunCount() {
return runCount.get();
}
+
+ public int getStartCount() {
+ return startCount.get();
+ }
}
private static final class PluginOperationTest extends PaymentOperation {
diff --git a/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java b/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java
index 06077cc..f2cd9c8 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 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:
*
@@ -25,6 +27,7 @@ import org.killbill.billing.ErrorCode;
import org.killbill.billing.payment.PaymentTestSuiteNoDB;
import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType;
+import org.killbill.billing.util.UUIDs;
import org.killbill.commons.request.Request;
import org.killbill.commons.request.RequestData;
import org.testng.Assert;
@@ -134,6 +137,7 @@ public class TestPluginDispatcher extends PaymentTestSuiteNoDB {
};
final CallableWithRequestData<PluginDispatcherReturnType<String>> callable = new CallableWithRequestData<PluginDispatcherReturnType<String>>(new RequestData(requestId),
+ UUIDs.getRandom(),
null,
null,
delegate);
diff --git a/payment/src/test/java/org/killbill/billing/payment/glue/MockEntitlementModuleForPayment.java b/payment/src/test/java/org/killbill/billing/payment/glue/MockEntitlementModuleForPayment.java
index ae2c6aa..46a82ab 100644
--- a/payment/src/test/java/org/killbill/billing/payment/glue/MockEntitlementModuleForPayment.java
+++ b/payment/src/test/java/org/killbill/billing/payment/glue/MockEntitlementModuleForPayment.java
@@ -43,7 +43,7 @@ public class MockEntitlementModuleForPayment extends MockEntitlementModule {
public void installBlockingApi() {
super.installBlockingApi();
- final BlockingState blockingState = new DefaultBlockingState(null, BlockingStateType.ACCOUNT, DefaultBlockingState.CLEAR_STATE_NAME, "test", false, false, false, new DateTime(DateTimeZone.UTC));
+ final BlockingState blockingState = new DefaultBlockingState(null, BlockingStateType.ACCOUNT, "CLEAR_STATE_NAME", "test", false, false, false, new DateTime(DateTimeZone.UTC));
Mockito.when(blockingApi.getBlockingAllForAccount(Mockito.<InternalTenantContext>any())).thenReturn(ImmutableList.<BlockingState>of(blockingState));
Mockito.when(blockingApi.getBlockingStateForService(Mockito.<UUID>any(), Mockito.<BlockingStateType>any(), Mockito.anyString(), Mockito.<InternalTenantContext>any())).thenReturn(blockingState);
}
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 8788e73..cc8c6e1 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
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * 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
@@ -27,6 +27,9 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.payment.api.PaymentMethodPlugin;
@@ -46,6 +49,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.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
@@ -68,6 +72,8 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
private final AtomicBoolean makeNextInvoiceFailWithException = new AtomicBoolean(false);
private final AtomicBoolean makeAllInvoicesFailWithError = new AtomicBoolean(false);
private final AtomicInteger makePluginWaitSomeMilliseconds = new AtomicInteger(0);
+ private final AtomicReference<BigDecimal> overrideNextProcessedAmount = new AtomicReference<BigDecimal>();
+ private final AtomicReference<Currency> overrideNextProcessedCurrency = new AtomicReference<Currency>();
private final Map<String, InternalPaymentInfo> payments = new ConcurrentHashMap<String, InternalPaymentInfo>();
private final Map<String, List<PaymentTransactionInfoPlugin>> paymentTransactions = new ConcurrentHashMap<String, List<PaymentTransactionInfoPlugin>>();
@@ -192,6 +198,7 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
makeAllInvoicesFailWithError.set(false);
makeNextInvoiceFailWithError.set(false);
makePluginWaitSomeMilliseconds.set(0);
+ overrideNextProcessedAmount.set(null);
paymentMethods.clear();
payments.clear();
paymentTransactions.clear();
@@ -214,6 +221,14 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
makePluginWaitSomeMilliseconds.set(milliseconds);
}
+ public void overrideNextProcessedAmount(final BigDecimal amount) {
+ overrideNextProcessedAmount.set(amount);
+ }
+
+ public void overrideNextProcessedCurrency(final Currency currency) {
+ overrideNextProcessedCurrency.set(currency);
+ }
+
public void updatePaymentTransactions(final UUID paymentId, final List<PaymentTransactionInfoPlugin> newTransactions) {
if (paymentTransactions.containsKey(paymentId.toString())) {
paymentTransactions.put (paymentId.toString(), newTransactions);
@@ -343,7 +358,7 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.REFUND, refundAmount, currency, properties);
}
- private PaymentTransactionInfoPlugin getPaymentTransactionInfoPluginResult(final UUID kbPaymentId, final UUID kbTransactionId, final TransactionType type, final BigDecimal amount, final Currency currency, final Iterable<PluginProperty> pluginProperties) throws PaymentPluginApiException {
+ private PaymentTransactionInfoPlugin getPaymentTransactionInfoPluginResult(final UUID kbPaymentId, final UUID kbTransactionId, final TransactionType type, final BigDecimal amount, @Nullable final Currency currency, final Iterable<PluginProperty> pluginProperties) throws PaymentPluginApiException {
if (makePluginWaitSomeMilliseconds.get() > 0) {
try {
Thread.sleep(makePluginWaitSomeMilliseconds.get());
@@ -379,7 +394,13 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
payments.put(kbPaymentId.toString(), info);
}
- final PaymentTransactionInfoPlugin result = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, type, amount, currency, clock.getUTCNow(), clock.getUTCNow(), status, errorCode, error);
+ final BigDecimal processedAmount = MoreObjects.firstNonNull(overrideNextProcessedAmount.getAndSet(null), amount);
+ Currency processedCurrency = overrideNextProcessedCurrency.getAndSet(null);
+ if (processedCurrency == null) {
+ processedCurrency = currency;
+ }
+
+ final PaymentTransactionInfoPlugin result = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, type, processedAmount, processedCurrency, clock.getUTCNow(), clock.getUTCNow(), status, errorCode, error);
List<PaymentTransactionInfoPlugin> existingTransactions = paymentTransactions.get(kbPaymentId.toString());
if (existingTransactions == null) {
existingTransactions = new ArrayList<PaymentTransactionInfoPlugin>();
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 f62e802..0ba6343 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
@@ -80,6 +80,8 @@ import static org.testng.Assert.fail;
public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
+ private static final int TIMEOUT = 10;
+
final PaymentOptions INVOICE_PAYMENT = new PaymentOptions() {
@Override
public boolean isExternalPayment() {
@@ -205,7 +207,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
- public void testCreateSuccessRefundPaymentControlWithItemAdjustments() throws PaymentApiException, InvoiceApiException, EventBusException {
+ public void testCreateSuccessRefundPaymentControlWithItemAdjustments() throws Exception {
final BigDecimal requestedAmount = BigDecimal.TEN;
final UUID subscriptionId = UUID.randomUUID();
@@ -263,13 +265,14 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(attempt2.getStateName(), "INIT");
clock.addDays(1);
- try {
- Thread.sleep(1500);
- } catch (InterruptedException e) {
- }
- final PaymentAttemptModelDao attempt3 = paymentDao.getPaymentAttempt(refundAttempt.getId(), internalCallContext);
- assertEquals(attempt3.getStateName(), "SUCCESS");
+ await().atMost(TIMEOUT, TimeUnit.SECONDS).until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ final PaymentAttemptModelDao attempt3 = paymentDao.getPaymentAttempt(refundAttempt.getId(), internalCallContext);
+ return "SUCCESS".equals(attempt3.getStateName());
+ }
+ });
}
@Test(groups = "slow")
@@ -418,7 +421,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(updatedPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
}
- // The test will check that when a PENDING entry stays PENDING, we go through all our retries and evebtually give up (no infinite loop of retries)
+ // The test will check that when a PENDING entry stays PENDING, we go through all our retries and eventually give up (no infinite loop of retries)
@Test(groups = "slow")
public void testPendingEntriesThatDontMove() throws Exception {
@@ -450,23 +453,24 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
testListener.assertListenerStatus();
// 15s,1m,3m,1h,1d,1d,1d,1d,1d
- for (TimeSpan cur : paymentConfig.getIncompleteTransactionsRetries()) {
+ for (final TimeSpan cur : paymentConfig.getIncompleteTransactionsRetries()) {
// Verify there is a notification to retry updating the value
assertEquals(getPendingNotificationCnt(internalCallContext), 1);
clock.addDeltaFromReality(cur.getMillis() + 1);
- // We add a sleep here to make sure the notification gets processed. Note that calling assertNotificationsCompleted would not work
- // because there is a point in time where the notification queue is empty (showing notification was processed), but the processing of the notification
+ assertNotificationsCompleted(internalCallContext, 5);
+ // We add a sleep here to make sure the notification gets processed. Note that calling assertNotificationsCompleted alone would not work
+ // because there is a point in time where the notification queue is empty (showing notification was processed), but the processing of the notification
// will itself enter a new notification, and so the synchronization is difficult without writing *too much code*.
Thread.sleep(1000);
+ assertNotificationsCompleted(internalCallContext, 5);
- //assertNotificationsCompleted(internalCallContext, 5);
final Payment updatedPayment = paymentApi.getPayment(payment.getId(), false, ImmutableList.<PluginProperty>of(), callContext);
Assert.assertEquals(updatedPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
}
- await().atMost(5, TimeUnit.SECONDS).until(new Callable<Boolean>() {
+ await().atMost(TIMEOUT, TimeUnit.SECONDS).until(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return getPendingNotificationCnt(internalCallContext) == 0;
@@ -515,8 +519,12 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
await().atMost(timeoutSec, SECONDS).until(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
- final List<NotificationEventWithMetadata<NotificationEvent>> notifications = notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, Janitor.QUEUE_NAME).getFutureOrInProcessingNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
- return notifications.isEmpty();
+ for (final NotificationEventWithMetadata<NotificationEvent> notificationEvent : notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, Janitor.QUEUE_NAME).getFutureOrInProcessingNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId())) {
+ if (!notificationEvent.getEffectiveDate().isAfter(clock.getUTCNow())) {
+ return false;
+ }
+ }
+ return true;
}
});
} catch (final Exception e) {
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
index cbc23e5..f289f57 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
@@ -58,6 +58,8 @@ import static org.testng.Assert.fail;
public class TestRetryService extends PaymentTestSuiteNoDB {
+ private static final int TIMEOUT = 10;
+
private MockPaymentProviderPlugin mockPaymentProviderPlugin;
@Override
@@ -174,7 +176,7 @@ public class TestRetryService extends PaymentTestSuiteNoDB {
moveClockForFailureType(FailureType.PAYMENT_FAILURE, 0);
try {
- await().atMost(5, SECONDS).until(new Callable<Boolean>() {
+ await().atMost(TIMEOUT, SECONDS).until(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
final List<PaymentAttemptModelDao> attempts = paymentDao.getPaymentAttempts(paymentExternalKey, internalCallContext);
@@ -260,7 +262,7 @@ public class TestRetryService extends PaymentTestSuiteNoDB {
final int curFailureCondition = curFailure;
try {
- await().atMost(5, SECONDS).until(new Callable<Boolean>() {
+ await().atMost(TIMEOUT, SECONDS).until(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
final List<PaymentAttemptModelDao> attempts = paymentDao.getPaymentAttempts(paymentExternalKey, internalCallContext);
@@ -345,7 +347,7 @@ public class TestRetryService extends PaymentTestSuiteNoDB {
final int curFailureCondition = curFailure;
try {
- await().atMost(5, SECONDS).until(new Callable<Boolean>() {
+ await().atMost(TIMEOUT, SECONDS).until(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
final List<PaymentAttemptModelDao> attempts = paymentDao.getPaymentAttempts(paymentExternalKey, internalCallContext);
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index 4d4317a..f79d4d0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>killbill-oss-parent</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.80</version>
+ <version>0.81</version>
</parent>
<artifactId>killbill</artifactId>
<version>0.16.2-SNAPSHOT</version>
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 3686716..9e3624c 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
@@ -40,7 +40,7 @@ import static org.testng.Assert.assertNotNull;
public abstract class KillbillClient extends GuicyKillbillTestSuiteWithEmbeddedDB {
- protected final int DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC = 5;
+ protected final int DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC = 10;
protected static final String PLUGIN_NAME = "noop";
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 09d899a..93b7374 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
@@ -156,7 +156,7 @@ public class TestPayment extends TestJaxrsBase {
refundTransaction.setTransactionExternalKey(refundTransactionExternalKey);
refundTransaction.setAmount(purchaseAmount);
refundTransaction.setCurrency(authPayment.getCurrency());
- final Payment refundPayment = killBillClient.refundPayment(refundTransaction, pluginProperties, createdBy, reason, comment);
+ final Payment refundPayment = killBillClient.refundPayment(refundTransaction, null, pluginProperties, createdBy, reason, comment);
verifyPaymentWithPendingRefund(account, paymentMethodId, paymentExternalKey, purchaseTransactionExternalKey, purchaseAmount, refundTransactionExternalKey, refundPayment);
// We cannot complete using just the payment id as JAX-RS doesn't know which transaction to complete
@@ -221,7 +221,7 @@ public class TestPayment extends TestJaxrsBase {
// Void payment using externalKey
final String voidTransactionExternalKey = UUID.randomUUID().toString();
- final Payment voidPayment = killBillClient.voidPayment(null, paymentExternalKey, voidTransactionExternalKey, ImmutableMap.<String, String>of(), createdBy, reason, comment);
+ final Payment voidPayment = killBillClient.voidPayment(null, paymentExternalKey, voidTransactionExternalKey, null, ImmutableMap.<String, String>of(), createdBy, reason, comment);
verifyPaymentTransaction(accountJson, voidPayment.getPaymentId(), paymentExternalKey, voidPayment.getTransactions().get(1),
voidTransactionExternalKey, null, "VOID", "SUCCESS");
}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java
index 9184c07..390e4ed 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java
@@ -71,7 +71,7 @@ public class TestPushNotification extends TestJaxrsBase {
}
private boolean waitForCallbacksToComplete() throws InterruptedException {
- long remainingMs = 20000;
+ long remainingMs = 30000;
do {
if (callbackCompleted) {
break;
diff --git a/util/src/test/java/org/killbill/billing/util/callcontext/TestCallContext.java b/util/src/test/java/org/killbill/billing/util/callcontext/TestCallContext.java
index 5798beb..43a57cf 100644
--- a/util/src/test/java/org/killbill/billing/util/callcontext/TestCallContext.java
+++ b/util/src/test/java/org/killbill/billing/util/callcontext/TestCallContext.java
@@ -1,7 +1,9 @@
/*
- * Copyright 2010-2011 Ning, Inc.
+ * Copyright 2010-2013 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:
*
@@ -34,6 +36,14 @@ public class TestCallContext implements CallContext {
this(userName, new DefaultClock().getUTCNow(), new DefaultClock().getUTCNow());
}
+ public TestCallContext(final CallContext context, final DateTime utcNow) {
+ this.userName = context.getUserName();
+ this.createdDate = utcNow;
+ this.updatedDate = utcNow;
+ this.userToken = context.getUserToken();
+ this.tenantId = context.getTenantId();
+ }
+
public TestCallContext(final String userName, final DateTime createdDate, final DateTime updatedDate) {
this.userName = userName;
this.createdDate = createdDate;