killbill-aplcache
Changes
entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java 7(+7 -0)
entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/EntitlementEventSqlDao.sql.stg 4(+0 -4)
invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java 2(+1 -1)
Details
diff --git a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
index 3740a57..8c3041a 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
@@ -227,7 +227,7 @@ public class TestAnalyticsService extends AnalyticsTestSuiteWithEmbeddedDB {
INVOICE_AMOUNT, ACCOUNT_CURRENCY);
invoice.addInvoiceItem(invoiceItem);
- invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), true, context);
final List<Invoice> invoices = invoiceDao.getInvoicesByAccount(account.getId());
Assert.assertEquals(invoices.size(), 1);
Assert.assertEquals(invoices.get(0).getInvoiceItems().size(), 1);
diff --git a/api/src/main/java/com/ning/billing/ErrorCode.java b/api/src/main/java/com/ning/billing/ErrorCode.java
index 8e000da..189d61e 100644
--- a/api/src/main/java/com/ning/billing/ErrorCode.java
+++ b/api/src/main/java/com/ning/billing/ErrorCode.java
@@ -203,6 +203,7 @@ public enum ErrorCode {
CHARGE_BACK_DOES_NOT_EXIST(4004, "Could not find chargeback for id %s."),
INVOICE_PAYMENT_BY_ATTEMPT_NOT_FOUND(4905, "No invoice payment could be found for paymentAttempt id %s."),
REFUND_AMOUNT_TOO_HIGH(4906, "Tried to refund %s of a %s payment."),
+ REFUND_AMOUNT_DONT_MATCH_ITEMS_TO_ADJUST(4907, "You can't specify a refund amount of %s that doesn't match the invoice items amount of %s."),
/*
*
diff --git a/api/src/main/java/com/ning/billing/invoice/api/InvoiceItemType.java b/api/src/main/java/com/ning/billing/invoice/api/InvoiceItemType.java
index 548bfd5..63cd82a 100644
--- a/api/src/main/java/com/ning/billing/invoice/api/InvoiceItemType.java
+++ b/api/src/main/java/com/ning/billing/invoice/api/InvoiceItemType.java
@@ -28,8 +28,8 @@ public enum InvoiceItemType {
// Credit adjustment, either at the account level (on its own invoice) or against an existing invoice
// (invoice level adjustment)
CREDIT_ADJ,
- // Invoice item adjustment
+ // Invoice item adjustment (by itself or triggered by a refund)
ITEM_ADJ,
- // Refund adjustment (against a posted payment)
+ // Refund adjustment (against a posted payment), used when adjusting invoices
REFUND_ADJ
}
diff --git a/api/src/main/java/com/ning/billing/invoice/api/InvoicePaymentApi.java b/api/src/main/java/com/ning/billing/invoice/api/InvoicePaymentApi.java
index a95c98e..fa0e738 100644
--- a/api/src/main/java/com/ning/billing/invoice/api/InvoicePaymentApi.java
+++ b/api/src/main/java/com/ning/billing/invoice/api/InvoicePaymentApi.java
@@ -18,6 +18,7 @@ package com.ning.billing.invoice.api;
import java.math.BigDecimal;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import org.joda.time.DateTime;
@@ -26,6 +27,7 @@ import com.ning.billing.catalog.api.Currency;
import com.ning.billing.util.callcontext.CallContext;
public interface InvoicePaymentApi {
+
/**
* @param accountId id of the account
* @return All invoices, including migrated invoices
@@ -42,7 +44,21 @@ public interface InvoicePaymentApi {
public void notifyOfPayment(UUID invoiceId, BigDecimal amountOutstanding, Currency currency, UUID paymentId, DateTime paymentDate, CallContext context);
- public InvoicePayment createRefund(UUID paymentId, BigDecimal amount, boolean isInvoiceAdjusted, UUID paymentCookieId, CallContext context) throws InvoiceApiException;
+ /**
+ * Create a refund.
+ *
+ *
+ * @param paymentId payment associated with that refund
+ * @param amount amount to refund
+ * @param isInvoiceAdjusted whether the refund should trigger an invoice or invoice item adjustment
+ * @param invoiceItemIdsWithAmounts invoice item ids and associated amounts to adjust
+ * @param paymentCookieId payment cookie id
+ * @param context the call context
+ * @return the created invoice payment object associated with this refund
+ * @throws InvoiceApiException
+ */
+ public InvoicePayment createRefund(UUID paymentId, BigDecimal amount, boolean isInvoiceAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts,
+ UUID paymentCookieId, CallContext context) throws InvoiceApiException;
public InvoicePayment createChargeback(UUID invoicePaymentId, BigDecimal amount, CallContext context) throws InvoiceApiException;
diff --git a/api/src/main/java/com/ning/billing/invoice/api/InvoiceUserApi.java b/api/src/main/java/com/ning/billing/invoice/api/InvoiceUserApi.java
index 38543f2..74fa8e3 100644
--- a/api/src/main/java/com/ning/billing/invoice/api/InvoiceUserApi.java
+++ b/api/src/main/java/com/ning/billing/invoice/api/InvoiceUserApi.java
@@ -22,8 +22,6 @@ import java.util.Collection;
import java.util.List;
import java.util.UUID;
-import javax.annotation.Nullable;
-
import org.joda.time.LocalDate;
import com.ning.billing.account.api.AccountApiException;
@@ -160,20 +158,33 @@ public interface InvoiceUserApi {
Currency currency, CallContext context) throws InvoiceApiException;
/**
- * Adjust a given invoice item.
+ * Adjust fully a given invoice item.
+ *
+ * @param accountId account id
+ * @param invoiceId invoice id
+ * @param invoiceItemId invoice item id
+ * @param effectiveDate the effective date for this adjustment invoice item (in the account timezone)
+ * @param context the call context
+ * @return the adjustment invoice item
+ * @throws InvoiceApiException
+ */
+ public InvoiceItem insertInvoiceItemAdjustment(UUID accountId, UUID invoiceId, UUID invoiceItemId, LocalDate effectiveDate, CallContext context) throws InvoiceApiException;
+
+ /**
+ * Adjust partially a given invoice item.
*
* @param accountId account id
* @param invoiceId invoice id
* @param invoiceItemId invoice item id
- * @param effectiveDate the effective date for this adjustment invoice item
- * @param amount the adjustment amount. Pass null to adjust for the full amount of the original item
- * @param currency adjustment currency. Pass null to use the original currency
+ * @param effectiveDate the effective date for this adjustment invoice item (in the account timezone)
+ * @param amount the adjustment amount
+ * @param currency adjustment currency
* @param context the call context
* @return the adjustment invoice item
* @throws InvoiceApiException
*/
public InvoiceItem insertInvoiceItemAdjustment(UUID accountId, UUID invoiceId, UUID invoiceItemId, LocalDate effectiveDate,
- @Nullable BigDecimal amount, @Nullable Currency currency, CallContext context) throws InvoiceApiException;
+ BigDecimal amount, Currency currency, CallContext context) throws InvoiceApiException;
/**
* Retrieve the invoice formatted in HTML.
diff --git a/api/src/main/java/com/ning/billing/payment/api/PaymentApi.java b/api/src/main/java/com/ning/billing/payment/api/PaymentApi.java
index dc7488c..bf38550 100644
--- a/api/src/main/java/com/ning/billing/payment/api/PaymentApi.java
+++ b/api/src/main/java/com/ning/billing/payment/api/PaymentApi.java
@@ -18,6 +18,7 @@ package com.ning.billing.payment.api;
import java.math.BigDecimal;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -35,7 +36,58 @@ public interface PaymentApi {
public Refund getRefund(final UUID refundId)
throws PaymentApiException;
- public Refund createRefund(final Account account, final UUID paymentId, final BigDecimal refundAmount, final boolean isAdjusted, final CallContext context)
+ /**
+ * Create a refund for a given payment. The associated invoice is not adjusted.
+ *
+ * @param account account to refund
+ * @param paymentId payment associated with that refund
+ * @param refundAmount amount to refund
+ * @param context the call context
+ * @return the created Refund
+ * @throws PaymentApiException
+ */
+ public Refund createRefund(final Account account, final UUID paymentId, final BigDecimal refundAmount, final CallContext context)
+ throws PaymentApiException;
+
+ /**
+ * Create a refund for a given payment. The associated invoice is adjusted.
+ *
+ * @param account account to refund
+ * @param paymentId payment associated with that refund
+ * @param refundAmount amount to refund
+ * @param context the call context
+ * @return the created Refund
+ * @throws PaymentApiException
+ */
+ public Refund createRefundWithAdjustment(final Account account, final UUID paymentId, final BigDecimal refundAmount, final CallContext context)
+ throws PaymentApiException;
+
+ /**
+ * Create a refund for a given payment. The specified invoice items are fully adjusted.
+ * The refund amount will be the sum of all invoice items amounts.
+ *
+ * @param account account to refund
+ * @param paymentId payment associated with that refund
+ * @param invoiceItemIds invoice item ids to adjust
+ * @param context the call context
+ * @return the created Refund
+ * @throws PaymentApiException
+ */
+ public Refund createRefundWithItemsAdjustments(final Account account, final UUID paymentId, final Set<UUID> invoiceItemIds, final CallContext context)
+ throws PaymentApiException;
+
+ /**
+ * Create a refund for a given payment. The specified invoice items are partially adjusted.
+ * The refund amount will be the sum of all adjustments.
+ *
+ * @param account account to refund
+ * @param paymentId payment associated with that refund
+ * @param invoiceItemIdsWithAmounts invoice item ids and associated amounts to adjust
+ * @param context the call context
+ * @return the created Refund
+ * @throws PaymentApiException
+ */
+ public Refund createRefundWithItemsAdjustments(final Account account, final UUID paymentId, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final CallContext context)
throws PaymentApiException;
public List<Refund> getAccountRefunds(final Account account)
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixModule.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixModule.java
index 96fcca7..ed59b21 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixModule.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixModule.java
@@ -34,6 +34,7 @@ import com.ning.billing.analytics.setup.AnalyticsModule;
import com.ning.billing.beatrix.integration.overdue.IntegrationTestOverdueModule;
import com.ning.billing.beatrix.lifecycle.DefaultLifecycle;
import com.ning.billing.beatrix.lifecycle.Lifecycle;
+import com.ning.billing.beatrix.util.InvoiceChecker;
import com.ning.billing.catalog.api.CatalogService;
import com.ning.billing.catalog.glue.CatalogModule;
import com.ning.billing.config.PaymentConfig;
@@ -105,6 +106,8 @@ public class BeatrixModule extends AbstractModule {
install(new PaymentPluginMockModule());
install(new DefaultJunctionModule());
install(new IntegrationTestOverdueModule());
+
+ bind(InvoiceChecker.class).asEagerSingleton();
}
private static final class PaymentPluginMockModule extends PaymentModule {
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java
index cad4fd1..8728ac9 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java
@@ -67,6 +67,9 @@ import static org.testng.Assert.assertTrue;
@Guice(modules = BeatrixModule.class)
public class TestAnalytics extends TestIntegrationBase {
+
+ private Account account;
+
private Plan subscriptionPlan;
@BeforeMethod(groups = "slow")
@@ -119,6 +122,11 @@ public class TestAnalytics extends TestIntegrationBase {
overdueWrapperFactory.setOverdueConfig(config);
busService.getBus().register(analyticsListener);
+
+ final DateTime initialDate = new DateTime(2012, 8, 1, 0, 15, 42, 0, testTimeZone);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ account = verifyAccountCreation(initialDate.plusDays(30).getDayOfMonth());
}
@AfterMethod(groups = "slow")
@@ -130,8 +138,6 @@ public class TestAnalytics extends TestIntegrationBase {
@Test(groups = "slow")
public void testCreateAndCancelSubscription() throws Exception {
- // Create an account
- final Account account = verifyAccountCreation();
// Create a bundle
final SubscriptionBundle bundle = verifyFirstBundle(account);
@@ -140,10 +146,10 @@ public class TestAnalytics extends TestIntegrationBase {
Subscription subscription = verifyFirstSubscription(account, bundle);
// Move after trial
- clock.addDeltaFromReality(AT_LEAST_ONE_MONTH_MS);
busHandler.pushExpectedEvent(TestApiListener.NextEvent.PHASE);
busHandler.pushExpectedEvent(TestApiListener.NextEvent.INVOICE);
busHandler.pushExpectedEvent(TestApiListener.NextEvent.PAYMENT);
+ clock.addDeltaFromReality(AT_LEAST_ONE_MONTH_MS);
Assert.assertTrue(busHandler.isCompleted(DELAY));
// Check BST - nothing should have changed
@@ -169,8 +175,6 @@ public class TestAnalytics extends TestIntegrationBase {
@Test(groups = "slow")
public void testCreateAndUpdateSubscription() throws Exception {
- // Create an account
- final Account account = verifyAccountCreation();
// Update some fields
verifyAccountUpdate(account);
@@ -190,10 +194,8 @@ public class TestAnalytics extends TestIntegrationBase {
@Test(groups = "slow")
public void testOverdue() throws Exception {
- paymentPlugin.makeAllInvoicesFailWithError(true);
- // Create an account
- final Account account = verifyAccountCreation();
+ paymentPlugin.makeAllInvoicesFailWithError(true);
// Create a bundle
final SubscriptionBundle bundle = verifyFirstBundle(account);
@@ -314,8 +316,9 @@ public class TestAnalytics extends TestIntegrationBase {
Assert.assertEquals(analyticsUserApi.getOverdueStatusesForBundle(bundle.getKey()).get(2).getAccountKey(), account.getExternalKey());
}
- private Account verifyAccountCreation() throws Exception {
- final AccountData accountData = getAccountData(1);
+ private Account verifyAccountCreation(int billCycleDay) throws Exception {
+
+ final AccountData accountData = getAccountData(billCycleDay);
// Verify BAC is empty
Assert.assertNull(analyticsUserApi.getAccountByKey(accountData.getExternalKey()));
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java
index cdd35dd..de00151 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java
@@ -21,6 +21,7 @@ import static org.testng.Assert.assertTrue;
import java.math.BigDecimal;
import java.util.List;
+import java.util.UUID;
import junit.framework.Assert;
@@ -29,8 +30,10 @@ import org.joda.time.LocalDate;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
+import com.google.common.collect.ImmutableList;
import com.ning.billing.account.api.Account;
import com.ning.billing.api.TestApiListener.NextEvent;
+import com.ning.billing.beatrix.util.InvoiceChecker.ExpectedItemCheck;
import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.PlanPhaseSpecifier;
import com.ning.billing.catalog.api.PriceListSet;
@@ -39,6 +42,7 @@ import com.ning.billing.entitlement.api.user.SubscriptionBundle;
import com.ning.billing.entitlement.api.user.SubscriptionData;
import com.ning.billing.invoice.api.Invoice;
import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
@Guice(modules = {BeatrixModule.class})
public class TestBundleTransfer extends TestIntegrationBase {
@@ -99,8 +103,8 @@ public class TestBundleTransfer extends TestIntegrationBase {
final List<InvoiceItem> invoiceItems = invoices.get(0).getInvoiceItems();
assertEquals(invoiceItems.size(), 1);
InvoiceItem theItem = invoiceItems.get(0);
- Assert.assertTrue(theItem.getStartDate().compareTo(new LocalDate(2012,05,11)) == 0);
- Assert.assertTrue(theItem.getEndDate().compareTo(new LocalDate(2013,05,11)) == 0);
+ Assert.assertTrue(theItem.getStartDate().compareTo(new LocalDate(2012,5,11)) == 0);
+ Assert.assertTrue(theItem.getEndDate().compareTo(new LocalDate(2013,5,11)) == 0);
Assert.assertTrue(theItem.getAmount().compareTo(new BigDecimal("2399.9500")) == 0);
}
@@ -159,8 +163,84 @@ public class TestBundleTransfer extends TestIntegrationBase {
final List<InvoiceItem> invoiceItems = invoices.get(0).getInvoiceItems();
assertEquals(invoiceItems.size(), 1);
InvoiceItem theItem = invoiceItems.get(0);
- Assert.assertTrue(theItem.getStartDate().compareTo(new LocalDate(2012,05,03)) == 0);
- Assert.assertTrue(theItem.getEndDate().compareTo(new LocalDate(2012,05,15)) == 0);
+ Assert.assertTrue(theItem.getStartDate().compareTo(new LocalDate(2012,5,3)) == 0);
+ Assert.assertTrue(theItem.getEndDate().compareTo(new LocalDate(2012,5,15)) == 0);
Assert.assertTrue(theItem.getAmount().compareTo(new BigDecimal("99.98")) == 0);
}
+
+ @Test(groups = "slow")
+ public void testBundleTransferWithBPMonthlyOnlyWIthCancellationImm() throws Exception {
+
+ final Account account = createAccountWithPaymentMethod(getAccountData(9));
+
+ // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+
+ final DateTime initialDate = new DateTime(2012, 4, 1, 0, 15, 42, 0, testTimeZone);
+
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+ final SubscriptionBundle bundle = entitlementUserApi.createBundleForAccount(account.getId(), "mycutebundle", context);
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+ //
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+ //
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.INVOICE);
+ final PlanPhaseSpecifier bpPlanPhaseSpecifier = new PlanPhaseSpecifier(productName, ProductCategory.BASE, term, planSetName, null);
+ final SubscriptionData bpSubscription = subscriptionDataFromSubscription(entitlementUserApi.createSubscription(bundle.getId(),
+ bpPlanPhaseSpecifier,
+ null,
+ context));
+ assertNotNull(bpSubscription);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+ assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId()).size(), 1);
+
+ assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId()).getCurrentPlan().getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ // Move out of trials for interesting invoices adjustments
+ busHandler.pushExpectedEvent(NextEvent.PHASE);
+ busHandler.pushExpectedEvent(NextEvent.INVOICE);
+ busHandler.pushExpectedEvent(NextEvent.PAYMENT);
+ clock.addDays(32);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ // BUNDLE TRANSFER
+ final Account newAccount = createAccountWithPaymentMethod(getAccountData(15));
+
+ busHandler.pushExpectedEvent(NextEvent.CANCEL);
+ busHandler.pushExpectedEvent(NextEvent.TRANSFER);
+ busHandler.pushExpectedEvent(NextEvent.INVOICE);
+ busHandler.pushExpectedEvent(NextEvent.INVOICE);
+ busHandler.pushExpectedEvent(NextEvent.PAYMENT);
+ transferApi.transferBundle(account.getId(), newAccount.getId(), "mycutebundle", clock.getUTCNow(), false, true, context);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ List<Invoice> invoices =invoiceUserApi.getInvoicesByAccount(account.getId());
+ assertEquals(invoices.size(), 3);
+
+
+ // CHECK OLD ACCOUNTS ITEMS
+ ImmutableList<ExpectedItemCheck> toBeChecked = ImmutableList.<ExpectedItemCheck>of(
+ new ExpectedItemCheck(new LocalDate(2012,5,1), new LocalDate(2012,5,9), InvoiceItemType.RECURRING, new BigDecimal("66.66")),
+ new ExpectedItemCheck(new LocalDate(2012,5,1), new LocalDate(2012,5,9), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-66.66")),
+ new ExpectedItemCheck(new LocalDate(2012,5,3), new LocalDate(2012,5,3), InvoiceItemType.CBA_ADJ, new BigDecimal("66.66")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedItemCheck>of(
+ new ExpectedItemCheck(new LocalDate(2012,5,1), new LocalDate(2012,5,3), InvoiceItemType.RECURRING, new BigDecimal("16.67")),
+ new ExpectedItemCheck(new LocalDate(2012,5,3), new LocalDate(2012,5,3), InvoiceItemType.CBA_ADJ, new BigDecimal("-16.67")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), toBeChecked);
+
+ // CHECK NEW ACCOUNT ITEMS
+ invoices =invoiceUserApi.getInvoicesByAccount(newAccount.getId());
+ assertEquals(invoices.size(), 1);
+
+ toBeChecked = ImmutableList.<ExpectedItemCheck>of(
+ new ExpectedItemCheck(new LocalDate(2012,5,3), new LocalDate(2012,5,15), InvoiceItemType.RECURRING, new BigDecimal("99.98")));
+ invoiceChecker.checkInvoice(invoices.get(0).getId(), toBeChecked);
+ }
}
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestEntitlement.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestEntitlement.java
index 57220ce..7d322e0 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestEntitlement.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestEntitlement.java
@@ -16,12 +16,17 @@
package com.ning.billing.beatrix.integration;
+import java.math.BigDecimal;
+import java.util.List;
+
import org.joda.time.LocalDate;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
+import com.google.common.collect.ImmutableList;
import com.ning.billing.account.api.Account;
import com.ning.billing.api.TestApiListener.NextEvent;
+import com.ning.billing.beatrix.util.InvoiceChecker.ExpectedItemCheck;
import com.ning.billing.catalog.api.ActionPolicy;
import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.PlanPhaseSpecifier;
@@ -29,6 +34,8 @@ import com.ning.billing.catalog.api.PriceListSet;
import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.entitlement.api.user.SubscriptionBundle;
import com.ning.billing.entitlement.api.user.SubscriptionData;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceItemType;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
@@ -68,21 +75,69 @@ public class TestEntitlement extends TestIntegrationBase {
assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId()).getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
// Move out of trials for interesting invoices adjustments
- busHandler.pushExpectedEvent(NextEvent.PHASE);
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
clock.addDays(40);
assertTrue(busHandler.isCompleted(DELAY));
assertListenerStatus();
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId());
+ assertEquals(invoices.size(), 2);
+ ImmutableList<ExpectedItemCheck> toBeChecked = ImmutableList.<ExpectedItemCheck>of(
+ new ExpectedItemCheck(new LocalDate(2012,5,1), new LocalDate(2013,5,1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), toBeChecked);
+
//
// FORCE AN IMMEDIATE CHANGE OF THE BILLING PERIOD
//
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE);
assertTrue(bpSubscription.changePlanWithPolicy(productName, BillingPeriod.MONTHLY, planSetName, clock.getUTCNow(), ActionPolicy.IMMEDIATE, context));
assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId()).getCurrentPlan().getBillingPeriod(), BillingPeriod.MONTHLY);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId());
+ assertEquals(invoices.size(), 3);
+ toBeChecked = ImmutableList.<ExpectedItemCheck>of(
+ new ExpectedItemCheck(new LocalDate(2012,5,1), new LocalDate(2013,5,1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+ new ExpectedItemCheck(new LocalDate(2012,5,1), new LocalDate(2013,5,1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2399.95")),
+ new ExpectedItemCheck(new LocalDate(2012,5,11), new LocalDate(2012,5,11), InvoiceItemType.CBA_ADJ, new BigDecimal("2399.95")));
+ invoiceChecker.checkInvoice(invoices.get(1).getId(), toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedItemCheck>of(
+ new ExpectedItemCheck(new LocalDate(2012,5,1), new LocalDate(2012,5,11), InvoiceItemType.RECURRING, new BigDecimal("65.76")),
+ new ExpectedItemCheck(new LocalDate(2012,5,11), new LocalDate(2012,6,1), InvoiceItemType.RECURRING, new BigDecimal("169.32")),
+ new ExpectedItemCheck(new LocalDate(2012,5,11), new LocalDate(2012,5,11), InvoiceItemType.CBA_ADJ, new BigDecimal("-235.08")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), toBeChecked);
+
+
//
// FORCE ANOTHER CHANGE
//
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE);
assertTrue(bpSubscription.changePlanWithPolicy(productName, BillingPeriod.ANNUAL, planSetName, clock.getUTCNow(), ActionPolicy.IMMEDIATE, context));
assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId()).getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
+ assertTrue(busHandler.isCompleted(DELAY));
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId());
+ assertEquals(invoices.size(), 4);
+
+
+ toBeChecked = ImmutableList.<ExpectedItemCheck>of(
+ new ExpectedItemCheck(new LocalDate(2012,5,1), new LocalDate(2012,5,11), InvoiceItemType.RECURRING, new BigDecimal("65.76")),
+ new ExpectedItemCheck(new LocalDate(2012,5,11), new LocalDate(2012,6,1), InvoiceItemType.RECURRING, new BigDecimal("169.32")),
+ new ExpectedItemCheck(new LocalDate(2012,5,11), new LocalDate(2012,5,11), InvoiceItemType.CBA_ADJ, new BigDecimal("-235.08")),
+ new ExpectedItemCheck(new LocalDate(2012,5,11), new LocalDate(2012,6,1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-169.32")),
+ new ExpectedItemCheck(new LocalDate(2012,5,11), new LocalDate(2012,5,11), InvoiceItemType.CBA_ADJ, new BigDecimal("169.32")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), toBeChecked);
+
+
+ toBeChecked = ImmutableList.<ExpectedItemCheck>of(
+ new ExpectedItemCheck(new LocalDate(2012,5,11), new LocalDate(2012,6,1), InvoiceItemType.RECURRING, new BigDecimal("137.76")),
+ new ExpectedItemCheck(new LocalDate(2012,5,11), new LocalDate(2012,5,11), InvoiceItemType.CBA_ADJ, new BigDecimal("-137.76")));
+ invoiceChecker.checkInvoice(invoices.get(3).getId(), toBeChecked);
+
+
}
}
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegration.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegration.java
index 36854d4..9bb0ed4 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegration.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegration.java
@@ -92,11 +92,14 @@ public class TestIntegration extends TestIntegrationBase {
//
busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.CANCEL, NextEvent.INVOICE);
bpSubscription.cancel(clock.getUTCNow(), false, context);
+
+ //Thread.sleep(10000000);
+
assertTrue(busHandler.isCompleted(DELAY));
assertListenerStatus();
final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId());
- assertEquals(invoices.size(), 3);
+ assertEquals(invoices.size(), 2);
// The first invoice is for the trial BP
assertEquals(invoices.get(0).getNumberOfItems(), 1);
assertEquals(invoices.get(0).getInvoiceItems().get(0).getStartDate().compareTo(today), 0);
@@ -120,8 +123,6 @@ public class TestIntegration extends TestIntegrationBase {
assertEquals(item.getAmount().compareTo(new BigDecimal("399.9500")), 0);
}
}
- // Null invoice
- assertEquals(invoices.get(2).getNumberOfItems(), 0);
}
@Test(groups = "slow")
@@ -520,9 +521,11 @@ public class TestIntegration extends TestIntegrationBase {
//waitForDebug();
assertTrue(busHandler.isCompleted(DELAY));
-
startDate = chargeThroughDate;
- DateTime endDate = chargeThroughDate.plusMonths(1);
+
+ // Resync latest with real CTD so test does not drift
+ DateTime realChargeThroughDate = entitlementUserApi.getSubscriptionFromId(subscription.getId()).getChargedThroughDate();
+ DateTime endDate = realChargeThroughDate;
price = subscription.getCurrentPhase().getRecurringPrice().getPrice(Currency.USD);
invoiceItemCount += 1;
verifyTestResult(accountId, subscription.getId(), startDate, endDate, price, endDate, invoiceItemCount);
@@ -558,7 +561,8 @@ public class TestIntegration extends TestIntegrationBase {
// MOVE AFTER CANCEL DATE AND EXPECT EVENT : NextEvent.CANCEL
busHandler.pushExpectedEvent(NextEvent.CANCEL);
- final Interval it = new Interval(clock.getUTCNow(), endDate);
+ realChargeThroughDate = entitlementUserApi.getSubscriptionFromId(subscription.getId()).getChargedThroughDate();
+ final Interval it = new Interval(clock.getUTCNow(), realChargeThroughDate.plusSeconds(5));
clock.addDeltaFromReality(it.toDurationMillis());
assertTrue(busHandler.isCompleted(DELAY));
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
index 4aa10d8..2339a8f 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
@@ -49,6 +49,7 @@ import com.ning.billing.api.TestApiListener;
import com.ning.billing.api.TestListenerStatus;
import com.ning.billing.beatrix.BeatrixTestSuiteWithEmbeddedDB;
import com.ning.billing.beatrix.lifecycle.Lifecycle;
+import com.ning.billing.beatrix.util.InvoiceChecker;
import com.ning.billing.catalog.api.Currency;
import com.ning.billing.dbi.MysqlTestingHelper;
import com.ning.billing.entitlement.api.EntitlementService;
@@ -97,7 +98,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
protected static final BigDecimal THIRTY_ONE = new BigDecimal("31.0000").setScale(NUMBER_OF_DECIMALS);
protected static final Logger log = LoggerFactory.getLogger(TestIntegration.class);
- protected static long AT_LEAST_ONE_MONTH_MS = 31L * 24L * 3600L * 1000L + 2000L;
+ protected static long AT_LEAST_ONE_MONTH_MS = 32L * 24L * 3600L * 1000L;
protected static final long DELAY = 5000;
@@ -161,6 +162,9 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
@Inject
protected AnalyticsListener analyticsListener;
+ @Inject
+ protected InvoiceChecker invoiceChecker;
+
protected TestApiListener busHandler;
private boolean isListenerFailed;
@@ -251,8 +255,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
log.info("Checking CTD: " + ctd.toString() + "; clock is " + clock.getUTCNow().toString());
// Either the ctd is today (start of the trial) or the clock is strictly before the CTD
assertTrue(clock.getUTCToday().compareTo(new LocalDate(ctd)) == 0 || clock.getUTCNow().isBefore(ctd));
- // The CTD is rounded too
- assertTrue(ctd.compareTo(new DateTime(chargeThroughDate.getYear(), chargeThroughDate.getMonthOfYear(), chargeThroughDate.getDayOfMonth(), 0, 0, testTimeZone)) == 0);
+ assertTrue(ctd.toDateTime(testTimeZone).toLocalDate().compareTo(new LocalDate(chargeThroughDate.getYear(), chargeThroughDate.getMonthOfYear(), chargeThroughDate.getDayOfMonth())) == 0);
}
protected SubscriptionData subscriptionDataFromSubscription(final Subscription sub) {
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/util/InvoiceChecker.java b/beatrix/src/test/java/com/ning/billing/beatrix/util/InvoiceChecker.java
new file mode 100644
index 0000000..dca65a5
--- /dev/null
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/util/InvoiceChecker.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package com.ning.billing.beatrix.util;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+
+import org.joda.time.LocalDate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+
+import com.google.inject.Inject;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
+import com.ning.billing.invoice.api.InvoiceUserApi;
+
+public class InvoiceChecker {
+
+ private final static Logger log = LoggerFactory.getLogger(InvoiceChecker.class);
+
+ private final InvoiceUserApi invoiceUserApi;
+
+ @Inject
+ public InvoiceChecker(final InvoiceUserApi invoiceUserApi) {
+ this.invoiceUserApi = invoiceUserApi;
+ }
+
+ public void checkInvoice(final UUID invoiceId, final List<ExpectedItemCheck> expected) {
+ final Invoice invoice =invoiceUserApi.getInvoice(invoiceId);
+ Assert.assertNotNull(invoice);
+
+ final List<InvoiceItem> actual = invoice.getInvoiceItems();
+ Assert.assertEquals(expected.size(), actual.size());
+ for (ExpectedItemCheck cur : expected) {
+ boolean found = false;
+ for (InvoiceItem in : actual) {
+ // Match first on type and start date
+ if (in.getInvoiceItemType() != cur.getType() || (in.getStartDate().compareTo(cur.getStartDate()) != 0)) {
+ continue;
+ }
+ if (in.getAmount().compareTo(cur.getAmount()) != 0) {
+ log.info(String.format("Found item type = %s and startDate = %s but amount differ (actual = %s, expected = %s) ",
+ cur.getType(), cur.getStartDate(), in.getAmount(), cur.getAmount()));
+ continue;
+ }
+
+ if ((cur.getEndDate() == null && in.getEndDate() == null) ||
+ (cur.getEndDate() != null && in.getEndDate() != null && cur.getEndDate().compareTo(in.getEndDate()) == 0)) {
+ found = true;
+ break;
+ }
+ log.info(String.format("Found item type = %s and startDate = %s, amount = %s but endDate differ (actual = %s, expected = %s) ",
+ cur.getType(), cur.getStartDate(), in.getAmount(), in.getEndDate(), cur.getEndDate()));
+ }
+ if (!found) {
+ Assert.fail(String.format("Failed to find invoice item type = %s and startDate = %s, amount = %s, endDate = %s",
+ cur.getType(), cur.getStartDate(), cur.getAmount(), cur.getEndDate()));
+ }
+ }
+ }
+
+ public static class ExpectedItemCheck {
+
+ private final LocalDate startDate;
+ private final LocalDate endDate;
+ private final InvoiceItemType type;
+ private final BigDecimal Amount;
+
+ public ExpectedItemCheck(final LocalDate startDate, final LocalDate endDate,
+ final InvoiceItemType type, final BigDecimal amount) {
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.type = type;
+ Amount = amount;
+ }
+
+ public LocalDate getStartDate() {
+ return startDate;
+ }
+ public LocalDate getEndDate() {
+ return endDate;
+ }
+ public InvoiceItemType getType() {
+ return type;
+ }
+ public BigDecimal getAmount() {
+ return Amount;
+ }
+ }
+
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java
index 455fe64..d2cd173 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java
@@ -705,6 +705,13 @@ public class AuditedEntitlementDao implements EntitlementDao {
final List<EntityAudit> audits = new ArrayList<EntityAudit>();
Long recordId;
final SubscriptionBundleData bundleData = bundleTransferData.getData();
+
+ final SubscriptionBundle existingBundle = transBundleDao.getBundleFromAccountAndKey(bundleData.getAccountId().toString(), bundleData.getKey());
+ if (existingBundle != null) {
+ log.error(String.format("Attempted to create a bundle for account %s and key %s that already existed, skip...", bundleData.getAccountId().toString(), bundleData.getKey()));
+ return;
+ }
+
for (final SubscriptionMigrationData curSubscription : bundleTransferData.getSubscriptions()) {
final SubscriptionData subData = curSubscription.getData();
for (final EntitlementEvent curEvent : curSubscription.getInitialEvents()) {
diff --git a/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/EntitlementEventSqlDao.sql.stg b/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/EntitlementEventSqlDao.sql.stg
index 09bf42d..f10428d 100644
--- a/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/EntitlementEventSqlDao.sql.stg
+++ b/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/EntitlementEventSqlDao.sql.stg
@@ -90,8 +90,6 @@ getFutureActiveEventForSubscription() ::= <<
and effective_date > :now
order by
effective_date asc
- , created_date asc
- , requested_date asc
, record_id asc
;
>>
@@ -103,8 +101,6 @@ getEventsForSubscription() ::= <<
subscription_id = :subscriptionId
order by
effective_date asc
- , created_date asc
- , requested_date asc
, record_id asc
;
>>
diff --git a/invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java b/invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
index effca13..0bd3a5c 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
@@ -19,6 +19,7 @@ package com.ning.billing.invoice.api.invoice;
import java.math.BigDecimal;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import org.joda.time.DateTime;
@@ -108,7 +109,8 @@ public class DefaultInvoicePaymentApi implements InvoicePaymentApi {
@Override
public InvoicePayment createRefund(final UUID paymentId, final BigDecimal amount, final boolean isInvoiceAdjusted,
- final UUID paymentCookieId, final CallContext context) throws InvoiceApiException {
+ final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final UUID paymentCookieId,
+ final CallContext context) throws InvoiceApiException {
return invoicePaymentWithException.executeAndThrow(new WithInvoiceApiExceptionCallback<InvoicePayment>() {
@@ -117,7 +119,7 @@ public class DefaultInvoicePaymentApi implements InvoicePaymentApi {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new InvoiceApiException(ErrorCode.PAYMENT_REFUND_AMOUNT_NEGATIVE_OR_NULL);
}
- return dao.createRefund(paymentId, amount, isInvoiceAdjusted, paymentCookieId, context);
+ return dao.createRefund(paymentId, amount, isInvoiceAdjusted, invoiceItemIdsWithAmounts, paymentCookieId, context);
}
});
}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java b/invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
index f007eac..f45509c 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
@@ -70,7 +70,7 @@ public class DefaultInvoiceMigrationApi implements InvoiceMigrationApi {
final InvoiceItem migrationInvoiceItem = new MigrationInvoiceItem(migrationInvoice.getId(), accountId, targetDate, balance, currency);
migrationInvoice.addInvoiceItem(migrationInvoiceItem);
- dao.create(migrationInvoice, account.getBillCycleDay().getDayOfMonthUTC(), context);
+ dao.create(migrationInvoice, account.getBillCycleDay().getDayOfMonthUTC(), true, context);
return migrationInvoice.getId();
}
}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java
index 2fc3623..927db0b 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -158,6 +158,11 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
}
@Override
+ public InvoiceItem insertInvoiceItemAdjustment(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId, final LocalDate effectiveDate, final CallContext context) throws InvoiceApiException {
+ return insertInvoiceItemAdjustment(accountId, invoiceId, invoiceItemId, effectiveDate, null, null, context);
+ }
+
+ @Override
public InvoiceItem insertInvoiceItemAdjustment(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId,
final LocalDate effectiveDate, @Nullable final BigDecimal amount,
@Nullable final Currency currency, final CallContext context) throws InvoiceApiException {
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
index 0de61e7..4fe738e 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
@@ -20,6 +20,7 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
@@ -57,9 +58,11 @@ import com.ning.billing.util.dao.ObjectType;
import com.ning.billing.util.dao.TableName;
import com.ning.billing.util.tag.ControlTagType;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableMap.Builder;
import com.google.inject.Inject;
public class DefaultInvoiceDao implements InvoiceDao {
@@ -157,7 +160,7 @@ public class DefaultInvoiceDao implements InvoiceDao {
}
@Override
- public void create(final Invoice invoice, final int billCycleDayUTC, final CallContext context) {
+ public void create(final Invoice invoice, final int billCycleDayUTC, final boolean isRealInvoice, final CallContext context) {
invoiceSqlDao.inTransaction(new Transaction<Void, InvoiceSqlDao>() {
@Override
public Void inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
@@ -165,9 +168,14 @@ public class DefaultInvoiceDao implements InvoiceDao {
if (currentInvoice == null) {
final List<EntityAudit> audits = new ArrayList<EntityAudit>();
- transactional.create(invoice, context);
- final Long recordId = transactional.getRecordId(invoice.getId().toString());
- audits.add(new EntityAudit(TableName.INVOICES, recordId, ChangeType.INSERT));
+ // We only want to insert that invoice if there are real invoiceItems associated to it-- if not, this is just
+ // a shell invoice and we only need to insert the invoiceItems-- for the already existing invoices
+
+ if (isRealInvoice) {
+ transactional.create(invoice, context);
+ final Long recordId = transactional.getRecordId(invoice.getId().toString());
+ audits.add(new EntityAudit(TableName.INVOICES, recordId, ChangeType.INSERT));
+ }
List<Long> recordIdList;
@@ -301,27 +309,31 @@ public class DefaultInvoiceDao implements InvoiceDao {
}
@Override
- public InvoicePayment createRefund(final UUID paymentId, final BigDecimal amount, final boolean isInvoiceAdjusted, final UUID paymentCookieId, final CallContext context)
+ public InvoicePayment createRefund(final UUID paymentId, final BigDecimal requestedRefundAmount, final boolean isInvoiceAdjusted,
+ final Map<UUID, BigDecimal> invoiceItemIdsWithNullAmounts, final UUID paymentCookieId,
+ final CallContext context)
throws InvoiceApiException {
return invoicePaymentSqlDao.inTransaction(new Transaction<InvoicePayment, InvoicePaymentSqlDao>() {
@Override
public InvoicePayment inTransaction(final InvoicePaymentSqlDao transactional, final TransactionStatus status) throws Exception {
+ final InvoiceSqlDao transInvoiceDao = transactional.become(InvoiceSqlDao.class);
+
final InvoicePayment payment = transactional.getByPaymentId(paymentId.toString());
if (payment == null) {
throw new InvoiceApiException(ErrorCode.INVOICE_PAYMENT_BY_ATTEMPT_NOT_FOUND, paymentId);
}
- final BigDecimal maxRefundAmount = payment.getAmount() == null ? BigDecimal.ZERO : payment.getAmount();
- final BigDecimal requestedPositiveAmount = amount == null ? maxRefundAmount : amount;
- // This check is good but not enough, we need to also take into account previous refunds
- // (But that should have been checked in the payment call already)
- if (requestedPositiveAmount.compareTo(maxRefundAmount) > 0) {
- throw new InvoiceApiException(ErrorCode.REFUND_AMOUNT_TOO_HIGH, requestedPositiveAmount, maxRefundAmount);
- }
- // Before we go further, check if that refund already got inserted -- the payment system keps a state machine
+ // Retrieve the amounts to adjust, if needed
+ final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts = computeItemAdjustments(payment.getInvoiceId().toString(),
+ transInvoiceDao,
+ invoiceItemIdsWithNullAmounts);
+
+ // Compute the actual amount to refund
+ final BigDecimal requestedPositiveAmount = computePositiveRefundAmount(payment, requestedRefundAmount, invoiceItemIdsWithAmounts);
+
+ // Before we go further, check if that refund already got inserted -- the payment system keeps a state machine
// and so this call may be called several time for the same paymentCookieId (which is really the refundId)
- //
final InvoicePayment existingRefund = transactional.getPaymentsForCookieId(paymentCookieId.toString());
if (existingRefund != null) {
return existingRefund;
@@ -333,7 +345,6 @@ public class DefaultInvoiceDao implements InvoiceDao {
transactional.create(refund, context);
// Retrieve invoice after the Refund
- final InvoiceSqlDao transInvoiceDao = transactional.become(InvoiceSqlDao.class);
final Invoice invoice = transInvoiceDao.getById(payment.getInvoiceId().toString());
if (invoice != null) {
populateChildren(invoice, transInvoiceDao);
@@ -344,8 +355,7 @@ public class DefaultInvoiceDao implements InvoiceDao {
final BigDecimal invoiceBalanceAfterRefund = invoice.getBalance();
final InvoiceItemSqlDao transInvoiceItemDao = transInvoiceDao.become(InvoiceItemSqlDao.class);
- // If we have an existing CBA > 0, we need to adjust it
- //final BigDecimal cbaAmountAfterRefund = invoice.getCBAAmount();
+ // If we have an existing CBA > 0 at the account level, we need to use it
final BigDecimal accountCbaAvailable = getAccountCBAFromTransaction(invoice.getAccountId(), transInvoiceDao);
BigDecimal cbaAdjAmount = BigDecimal.ZERO;
if (accountCbaAvailable.compareTo(BigDecimal.ZERO) > 0) {
@@ -355,19 +365,102 @@ public class DefaultInvoiceDao implements InvoiceDao {
}
final BigDecimal requestedPositiveAmountAfterCbaAdj = requestedPositiveAmount.add(cbaAdjAmount);
- if (isInvoiceAdjusted) {
+ // At this point, we created the refund which made the invoice balance positive and applied any existing
+ // available CBA to that invoice.
+ // We now need to adjust the invoice and/or invoice items if needed and specified.
+ if (isInvoiceAdjusted && invoiceItemIdsWithAmounts.size() == 0) {
+ // Invoice adjustment
final BigDecimal maxBalanceToAdjust = (invoiceBalanceAfterRefund.compareTo(BigDecimal.ZERO) <= 0) ? BigDecimal.ZERO : invoiceBalanceAfterRefund;
final BigDecimal requestedPositiveAmountToAdjust = requestedPositiveAmountAfterCbaAdj.compareTo(maxBalanceToAdjust) > 0 ? maxBalanceToAdjust : requestedPositiveAmountAfterCbaAdj;
if (requestedPositiveAmountToAdjust.compareTo(BigDecimal.ZERO) > 0) {
final InvoiceItem adjItem = new RefundAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(), requestedPositiveAmountToAdjust.negate(), invoice.getCurrency());
transInvoiceItemDao.create(adjItem, context);
}
+ } else if (isInvoiceAdjusted) {
+ // Invoice item adjustment
+ for (final UUID invoiceItemId : invoiceItemIdsWithAmounts.keySet()) {
+ final BigDecimal adjAmount = invoiceItemIdsWithAmounts.get(invoiceItemId);
+ final InvoiceItem item = createAdjustmentItem(transInvoiceDao, invoice.getId(), invoiceItemId, adjAmount,
+ invoice.getCurrency(), context.getCreatedDate().toLocalDate());
+ transInvoiceItemDao.create(item, context);
+ }
}
+
return refund;
}
});
}
+ /**
+ * Find amounts to adjust for individual items, if not specified.
+ * The user gives us a list of items to adjust associated with a given amount (how much to refund per invoice item).
+ * In case of full adjustments, the amount can be null: in this case, we retrieve the original amount for the invoice
+ * item.
+ *
+ * @param invoiceId original invoice id
+ * @param transInvoiceDao the transactional InvoiceSqlDao
+ * @param invoiceItemIdsWithNullAmounts the original mapping between invoice item ids and amount to refund (contains null)
+ * @return the final mapping between invoice item ids and amount to refund
+ * @throws InvoiceApiException
+ */
+ private Map<UUID, BigDecimal> computeItemAdjustments(final String invoiceId, final InvoiceSqlDao transInvoiceDao,
+ final Map<UUID, BigDecimal> invoiceItemIdsWithNullAmounts) throws InvoiceApiException {
+ // Populate the missing amounts for individual items, if needed
+ final Builder<UUID, BigDecimal> invoiceItemIdsWithAmountsBuilder = new Builder<UUID, BigDecimal>();
+ if (invoiceItemIdsWithNullAmounts.size() == 0) {
+ return invoiceItemIdsWithAmountsBuilder.build();
+ }
+
+ // Retrieve invoice before the Refund
+ final Invoice invoice = transInvoiceDao.getById(invoiceId);
+ if (invoice != null) {
+ populateChildren(invoice, transInvoiceDao);
+ } else {
+ throw new IllegalStateException("Invoice shouldn't be null for id " + invoiceId);
+ }
+
+ for (final UUID invoiceItemId : invoiceItemIdsWithNullAmounts.keySet()) {
+ final BigDecimal adjAmount = Objects.firstNonNull(invoiceItemIdsWithNullAmounts.get(invoiceItemId),
+ getInvoiceItemAmountForId(invoice, invoiceItemId));
+ invoiceItemIdsWithAmountsBuilder.put(invoiceItemId, adjAmount);
+ }
+
+ return invoiceItemIdsWithAmountsBuilder.build();
+ }
+
+ private BigDecimal getInvoiceItemAmountForId(final Invoice invoice, final UUID invoiceItemId) throws InvoiceApiException {
+ for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
+ if (invoiceItem.getId().equals(invoiceItemId)) {
+ return invoiceItem.getAmount();
+ }
+ }
+
+ throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_NOT_FOUND, invoiceItemId);
+ }
+
+ @VisibleForTesting
+ BigDecimal computePositiveRefundAmount(final InvoicePayment payment, final BigDecimal requestedAmount, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts) throws InvoiceApiException {
+ final BigDecimal maxRefundAmount = payment.getAmount() == null ? BigDecimal.ZERO : payment.getAmount();
+ final BigDecimal requestedPositiveAmount = requestedAmount == null ? maxRefundAmount : requestedAmount;
+ // This check is good but not enough, we need to also take into account previous refunds
+ // (But that should have been checked in the payment call already)
+ if (requestedPositiveAmount.compareTo(maxRefundAmount) > 0) {
+ throw new InvoiceApiException(ErrorCode.REFUND_AMOUNT_TOO_HIGH, requestedPositiveAmount, maxRefundAmount);
+ }
+
+ // Verify if the requested amount matches the invoice items to adjust, if specified
+ BigDecimal amountFromItems = BigDecimal.ZERO;
+ for (final BigDecimal itemAmount : invoiceItemIdsWithAmounts.values()) {
+ amountFromItems = amountFromItems.add(itemAmount);
+ }
+
+ // Sanity check: if some items were specified, then the sum should be equal to specified refund amount, if specified
+ if (amountFromItems.compareTo(BigDecimal.ZERO) != 0 && requestedPositiveAmount.compareTo(amountFromItems) != 0) {
+ throw new InvoiceApiException(ErrorCode.REFUND_AMOUNT_DONT_MATCH_ITEMS_TO_ADJUST, requestedPositiveAmount, amountFromItems);
+ }
+ return requestedPositiveAmount;
+ }
+
@Override
public InvoicePayment postChargeback(final UUID invoicePaymentId, final BigDecimal amount, final CallContext context) throws InvoiceApiException {
@@ -438,8 +531,7 @@ public class DefaultInvoiceDao implements InvoiceDao {
@Override
public InvoiceItem insertCredit(final UUID accountId, final UUID invoiceId, final BigDecimal positiveCreditAmount,
- final LocalDate effectiveDate, final Currency currency,
- final CallContext context) {
+ final LocalDate effectiveDate, final Currency currency, final CallContext context) {
return invoiceSqlDao.inTransaction(new Transaction<InvoiceItem, InvoiceSqlDao>() {
@Override
public InvoiceItem inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
@@ -453,7 +545,7 @@ public class DefaultInvoiceDao implements InvoiceDao {
// Note! The amount is negated here!
final InvoiceItem credit = new CreditAdjInvoiceItem(invoiceIdForCredit, accountId, effectiveDate, positiveCreditAmount.negate(), currency);
- createItemAndAddCBAIfNeeded(transactional, credit, context);
+ insertItemAndAddCBAIfNeeded(transactional, credit, context);
return credit;
}
});
@@ -466,28 +558,9 @@ public class DefaultInvoiceDao implements InvoiceDao {
return invoiceSqlDao.inTransaction(new Transaction<InvoiceItem, InvoiceSqlDao>() {
@Override
public InvoiceItem inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
- // First, retrieve the invoice item in question
- final InvoiceItemSqlDao invoiceItemSqlDao = transactional.become(InvoiceItemSqlDao.class);
- final InvoiceItem invoiceItemToBeAdjusted = invoiceItemSqlDao.getById(invoiceItemId.toString());
- if (invoiceItemToBeAdjusted == null) {
- throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_NOT_FOUND, invoiceItemId);
- }
-
- // Validate the invoice it belongs to
- if (!invoiceItemToBeAdjusted.getInvoiceId().equals(invoiceId)) {
- throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_FOR_INVOICE_ITEM_ADJUSTMENT, invoiceItemId, invoiceId);
- }
-
- // Retrieve the amount and currency if needed
- final BigDecimal amountToRefund = Objects.firstNonNull(positiveAdjAmount, invoiceItemToBeAdjusted.getAmount());
- // TODO - should we enforce the currency (and respect the original one) here if the amount passed was null?
- final Currency currencyForAdjustment = Objects.firstNonNull(currency, invoiceItemToBeAdjusted.getCurrency());
-
- // Finally, create the adjustment
- // Note! The amount is negated here!
- final InvoiceItem invoiceItemAdjustment = new ItemAdjInvoiceItem(invoiceItemToBeAdjusted, effectiveDate,
- amountToRefund.negate(), currencyForAdjustment);
- createItemAndAddCBAIfNeeded(transactional, invoiceItemAdjustment, context);
+ final InvoiceItem invoiceItemAdjustment = createAdjustmentItem(transactional, invoiceId, invoiceItemId, positiveAdjAmount,
+ currency, effectiveDate);
+ insertItemAndAddCBAIfNeeded(transactional, invoiceItemAdjustment, context);
return invoiceItemAdjustment;
}
});
@@ -499,25 +572,71 @@ public class DefaultInvoiceDao implements InvoiceDao {
}
/**
+ * Create an adjustment for a given invoice item. This just creates the object in memory, it doesn't write it to disk.
+ *
+ * @param invoiceId the invoice id
+ * @param invoiceItemId the invoice item id to adjust
+ * @param effectiveDate adjustment effective date, in the account timezone
+ * @param positiveAdjAmount the amount to adjust. Pass null to adjust the full amount of the original item
+ * @param currency the currency of the amount. Pass null to default to the original currency used
+ * @return the adjustment item
+ */
+ private InvoiceItem createAdjustmentItem(final InvoiceSqlDao transactional, final UUID invoiceId, final UUID invoiceItemId,
+ final BigDecimal positiveAdjAmount, final Currency currency, final LocalDate effectiveDate) throws InvoiceApiException {
+ // First, retrieve the invoice item in question
+ final InvoiceItemSqlDao invoiceItemSqlDao = transactional.become(InvoiceItemSqlDao.class);
+ final InvoiceItem invoiceItemToBeAdjusted = invoiceItemSqlDao.getById(invoiceItemId.toString());
+ if (invoiceItemToBeAdjusted == null) {
+ throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_NOT_FOUND, invoiceItemId);
+ }
+
+ // Validate the invoice it belongs to
+ if (!invoiceItemToBeAdjusted.getInvoiceId().equals(invoiceId)) {
+ throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_FOR_INVOICE_ITEM_ADJUSTMENT, invoiceItemId, invoiceId);
+ }
+
+ // Retrieve the amount and currency if needed
+ final BigDecimal amountToAdjust = Objects.firstNonNull(positiveAdjAmount, invoiceItemToBeAdjusted.getAmount());
+ // TODO - should we enforce the currency (and respect the original one) here if the amount passed was null?
+ final Currency currencyForAdjustment = Objects.firstNonNull(currency, invoiceItemToBeAdjusted.getCurrency());
+
+ // Finally, create the adjustment
+ // Note! The amount is negated here!
+ return new ItemAdjInvoiceItem(invoiceItemToBeAdjusted, effectiveDate, amountToAdjust.negate(), currencyForAdjustment);
+ }
+
+ /**
* Create an invoice item and adjust the invoice with a CBA item if the new invoice balance is negative.
*
* @param transactional the InvoiceSqlDao
* @param item the invoice item to create
* @param context the call context
*/
- private void createItemAndAddCBAIfNeeded(final InvoiceSqlDao transactional, final InvoiceItem item, final CallContext context) {
+ private void insertItemAndAddCBAIfNeeded(final InvoiceSqlDao transactional, final InvoiceItem item, final CallContext context) {
final InvoiceItemSqlDao transInvoiceItemDao = transactional.become(InvoiceItemSqlDao.class);
transInvoiceItemDao.create(item, context);
- final Invoice invoice = transactional.getById(item.getInvoiceId().toString());
+ addCBAIfNeeded(transactional, item.getInvoiceId(), context);
+ }
+
+ /**
+ * Adjust the invoice with a CBA item if the new invoice balance is negative.
+ *
+ * @param transactional the InvoiceSqlDao
+ * @param invoiceId the invoice id to adjust
+ * @param context the call context
+ */
+ private void addCBAIfNeeded(final InvoiceSqlDao transactional, final UUID invoiceId, final CallContext context) {
+ final Invoice invoice = transactional.getById(invoiceId.toString());
if (invoice != null) {
populateChildren(invoice, transactional);
} else {
- throw new IllegalStateException("Invoice shouldn't be null for this item at this stage " + item.getInvoiceId());
+ throw new IllegalStateException("Invoice shouldn't be null for this item at this stage " + invoiceId);
}
// If invoice balance becomes negative we add some CBA item
if (invoice.getBalance().compareTo(BigDecimal.ZERO) < 0) {
+ final InvoiceItemSqlDao transInvoiceItemDao = transactional.become(InvoiceItemSqlDao.class);
final InvoiceItem cbaAdjItem = new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(),
invoice.getBalance().negate(), invoice.getCurrency());
transInvoiceItemDao.create(cbaAdjItem, context);
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
index 8a352a3..a9104f9 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
@@ -18,6 +18,7 @@ package com.ning.billing.invoice.dao;
import java.math.BigDecimal;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
@@ -34,7 +35,7 @@ import com.ning.billing.util.callcontext.CallContext;
public interface InvoiceDao {
- void create(final Invoice invoice, final int billCycleDayUTC, final CallContext context);
+ void create(final Invoice invoice, final int billCycleDayUTC, final boolean isRealInvoice, final CallContext context);
Invoice getById(final UUID id);
@@ -70,7 +71,21 @@ public interface InvoiceDao {
InvoicePayment postChargeback(final UUID invoicePaymentId, final BigDecimal amount, final CallContext context) throws InvoiceApiException;
- InvoicePayment createRefund(UUID paymentId, BigDecimal amount, boolean isInvoiceAdjusted, UUID paymentCookieId, CallContext context) throws InvoiceApiException;
+ /**
+ * Create a refund.
+ *
+ *
+ * @param paymentId payment associated with that refund
+ * @param amount amount to refund
+ * @param isInvoiceAdjusted whether the refund should trigger an invoice or invoice item adjustment
+ * @param invoiceItemIdsWithAmounts invoice item ids and associated amounts to adjust
+ * @param paymentCookieId payment cookie id
+ * @param context the call context
+ * @return the created invoice payment object associated with this refund
+ * @throws InvoiceApiException
+ */
+ InvoicePayment createRefund(final UUID paymentId, final BigDecimal amount, final boolean isInvoiceAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts,
+ final UUID paymentCookieId, final CallContext context) throws InvoiceApiException;
BigDecimal getRemainingAmountPaid(final UUID invoicePaymentId);
diff --git a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
index 694330d..087cf4a 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
@@ -62,6 +62,8 @@ import com.ning.billing.util.globallocker.GlobalLocker;
import com.ning.billing.util.globallocker.GlobalLocker.LockerType;
import com.ning.billing.util.globallocker.LockFailedException;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
import com.google.inject.Inject;
public class InvoiceDispatcher {
@@ -178,7 +180,16 @@ public class InvoiceDispatcher {
}
outputDebugData(billingEvents, invoices);
if (!dryRun) {
- invoiceDao.create(invoice, account.getBillCycleDay().getDayOfMonthUTC(), context);
+
+ // We need to check whether this is just a 'shell' invoice or a real invoice with items on it
+ final boolean isRealInvoiceWithItems = Collections2.filter(invoice.getInvoiceItems(), new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(InvoiceItem input) {
+ return input.getInvoiceId().equals(invoice.getId());
+ }
+ }).size() > 0;
+
+ invoiceDao.create(invoice, account.getBillCycleDay().getDayOfMonthUTC(), isRealInvoiceWithItems, context);
final List<InvoiceItem> fixedPriceInvoiceItems = invoice.getInvoiceItems(FixedPriceInvoiceItem.class);
final List<InvoiceItem> recurringInvoiceItems = invoice.getInvoiceItems(RecurringInvoiceItem.class);
@@ -236,16 +247,17 @@ public class InvoiceDispatcher {
final UUID subscriptionId = item.getSubscriptionId();
final DateTime endDate;
if (item.getEndDate() != null) {
- endDate = new DateTime(item.getEndDate().toDateTimeAtStartOfDay(), DateTimeZone.UTC);
+ endDate = new DateTime(item.getEndDate().toDateTime(clock.getUTCNow()) , DateTimeZone.UTC);
} else {
// item end date is null for fixed price items for instance
- endDate = new DateTime(item.getStartDate().toDateTimeAtStartOfDay(), DateTimeZone.UTC);
+ endDate = new DateTime(item.getStartDate().toDateTime(clock.getUTCNow()), DateTimeZone.UTC);
}
if (chargeThroughDates.containsKey(subscriptionId)) {
if (chargeThroughDates.get(subscriptionId).isBefore(endDate)) {
// The CTD should always align with the BCD
- chargeThroughDates.put(subscriptionId, InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(endDate, billCycleDay.getDayOfMonthLocal()));
+ final DateTime ctd = InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(endDate, billCycleDay.getDayOfMonthLocal());
+ chargeThroughDates.put(subscriptionId, ctd);
}
} else {
chargeThroughDates.put(subscriptionId, endDate);
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java b/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
index 07e385b..69193f6 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
@@ -53,10 +53,28 @@ public class InAdvanceBillingMode implements BillingMode {
final List<RecurringInvoiceItemData> results = new ArrayList<RecurringInvoiceItemData>();
- // beginning from the start date, find the first billing date
final LocalDate firstBillingCycleDate = calculateBillingCycleDateOnOrAfter(startDate, accountTimeZone, billingCycleDayLocal);
- // add pro-ration item if needed
+ // We are not billing for less than a day (we could...)
+ if (endDate != null && endDate.equals(startDate)) {
+ return results;
+ }
+ //
+ // If there is an endDate and that endDate is before our first coming firstBillingCycleDate, all we have to do
+ // is to charge for that period
+ //
+ if (endDate != null && !endDate.isAfter(firstBillingCycleDate)) {
+ final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, endDate, billingPeriod);
+ final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, endDate, leadingProRationPeriods);
+ results.add(itemData);
+ return results;
+ }
+
+ //
+ // Leading proration if
+ // i) The first firstBillingCycleDate is strictly after our start date AND
+ // ii) The endDate is is not null and is strictly after our firstBillingCycleDate (previous check)
+ //
if (firstBillingCycleDate.isAfter(startDate)) {
final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, firstBillingCycleDate, billingPeriod);
if (leadingProRationPeriods != null && leadingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) {
@@ -67,7 +85,11 @@ public class InAdvanceBillingMode implements BillingMode {
}
}
- // add one item per billing period
+ //
+ // Calculate the effectiveEndDate from the firstBillingCycleDate:
+ // - If endDate != null and targetDate is after endDate => this is the endDate and will lead to a trailing pro-ration
+ // - If not, this is the last billingCycleDate calculation right after the targetDate
+ //
final LocalDate effectiveEndDate;
if (endDate != null) {
effectiveEndDate = calculateEffectiveEndDate(firstBillingCycleDate, targetDate, endDate, billingPeriod);
@@ -75,6 +97,9 @@ public class InAdvanceBillingMode implements BillingMode {
effectiveEndDate = calculateEffectiveEndDate(firstBillingCycleDate, targetDate, billingPeriod);
}
+ //
+ // Based on what we calculated previously, code recompute one more time the numberOfWholeBillingPeriods
+ //
final LocalDate lastBillingCycleDate = calculateLastBillingCycleDateBefore(effectiveEndDate, firstBillingCycleDate, billingCycleDayLocal, billingPeriod);
final int numberOfWholeBillingPeriods = calculateNumberOfWholeBillingPeriods(firstBillingCycleDate, lastBillingCycleDate, billingPeriod);
final int numberOfMonthsPerBillingPeriod = billingPeriod.getNumberOfMonths();
@@ -97,7 +122,9 @@ public class InAdvanceBillingMode implements BillingMode {
results.add(new RecurringInvoiceItemData(servicePeriodStartDate, servicePeriodEndDate, BigDecimal.ONE));
}
- // check to see if a trailing pro-ration amount is needed
+ //
+ // Now we check if indeed we need a trailing proration and add that incomplete item
+ //
if (effectiveEndDate.isAfter(lastBillingCycleDate)) {
final BigDecimal trailingProRationPeriods = calculateProRationAfterLastBillingCycleDate(effectiveEndDate, lastBillingCycleDate, billingPeriod);
if (trailingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) {
diff --git a/invoice/src/main/resources/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg b/invoice/src/main/resources/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
index baec2d4..a4ee86c 100644
--- a/invoice/src/main/resources/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
+++ b/invoice/src/main/resources/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
@@ -117,14 +117,13 @@ getChargeBacksByAccountId() ::= <<
SELECT <invoicePaymentFields("ip.")>
FROM invoice_payments ip
INNER JOIN invoices i ON i.id = ip.invoice_id
- WHERE i.account_id = :accountId
- AND linked_invoice_payment_id IS NOT NULL;
+ WHERE ip.type = 'CHARGED_BACK' AND i.account_id = :accountId;
>>
getChargebacksByPaymentId() ::= <<
SELECT <invoicePaymentFields()>
FROM invoice_payments
- WHERE linked_invoice_payment_id IN
+ WHERE type = 'CHARGED_BACK' AND linked_invoice_payment_id IN
(SELECT id FROM invoice_payments WHERE payment_id = :paymentId);
>>
;
diff --git a/invoice/src/test/java/com/ning/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java b/invoice/src/test/java/com/ning/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
new file mode 100644
index 0000000..9ccbf84
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.invoice.api.invoice;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.IDBI;
+import org.testng.Assert;
+import org.testng.annotations.BeforeSuite;
+import org.testng.annotations.Test;
+
+import com.ning.billing.KillbillTestSuiteWithEmbeddedDB;
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.dbi.MysqlTestingHelper;
+import com.ning.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceApiException;
+import com.ning.billing.invoice.api.InvoicePayment;
+import com.ning.billing.invoice.api.InvoicePayment.InvoicePaymentType;
+import com.ning.billing.invoice.api.InvoicePaymentApi;
+import com.ning.billing.invoice.dao.DefaultInvoiceDao;
+import com.ning.billing.invoice.dao.InvoiceDao;
+import com.ning.billing.invoice.dao.InvoiceItemSqlDao;
+import com.ning.billing.invoice.dao.InvoiceSqlDao;
+import com.ning.billing.invoice.notification.MockNextBillingDatePoster;
+import com.ning.billing.invoice.notification.NextBillingDatePoster;
+import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.TestCallContext;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+import com.ning.billing.util.tag.api.DefaultTagUserApi;
+import com.ning.billing.util.tag.dao.MockTagDao;
+import com.ning.billing.util.tag.dao.MockTagDefinitionDao;
+import com.ning.billing.util.tag.dao.TagDao;
+import com.ning.billing.util.tag.dao.TagDefinitionDao;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import static com.ning.billing.invoice.tests.InvoiceTestUtils.createAndPersistInvoice;
+import static com.ning.billing.invoice.tests.InvoiceTestUtils.createAndPersistPayment;
+
+public class TestDefaultInvoicePaymentApi extends InvoiceTestSuiteWithEmbeddedDB {
+
+ private static final BigDecimal THIRTY = new BigDecimal("30.00");
+ private static final Currency CURRENCY = Currency.EUR;
+
+ private final Clock clock = new ClockMock();
+
+ private InvoiceSqlDao invoiceSqlDao;
+ private InvoiceItemSqlDao invoiceItemSqlDao;
+ private InvoicePaymentApi invoicePaymentApi;
+ private CallContext context;
+
+ @BeforeSuite(groups = "slow")
+ public void setup() throws IOException {
+ final MysqlTestingHelper helper = KillbillTestSuiteWithEmbeddedDB.getMysqlTestingHelper();
+ final IDBI dbi = helper.getDBI();
+
+ invoiceSqlDao = dbi.onDemand(InvoiceSqlDao.class);
+ invoiceSqlDao.test();
+
+ invoiceItemSqlDao = dbi.onDemand(InvoiceItemSqlDao.class);
+ invoiceItemSqlDao.test();
+
+ final NextBillingDatePoster nextBillingDatePoster = new MockNextBillingDatePoster();
+ final TagDefinitionDao tagDefinitionDao = new MockTagDefinitionDao();
+ final TagDao tagDao = new MockTagDao();
+ final TagUserApi tagUserApi = new DefaultTagUserApi(tagDefinitionDao, tagDao);
+ final InvoiceDao invoiceDao = new DefaultInvoiceDao(dbi, nextBillingDatePoster, tagUserApi, clock);
+ invoicePaymentApi = new DefaultInvoicePaymentApi(invoiceDao);
+
+ context = new TestCallContext("Invoice payment tests");
+ }
+
+ @Test(groups = "slow")
+ public void testFullRefundWithNoAdjustment() throws Exception {
+ verifyRefund(THIRTY, THIRTY, THIRTY, false, ImmutableMap.<UUID, BigDecimal>of());
+ }
+
+ @Test(groups = "slow")
+ public void testPartialRefundWithNoAdjustment() throws Exception {
+ verifyRefund(THIRTY, BigDecimal.TEN, BigDecimal.TEN, false, ImmutableMap.<UUID, BigDecimal>of());
+ }
+
+ @Test(groups = "slow")
+ public void testFullRefundWithInvoiceAdjustment() throws Exception {
+ verifyRefund(THIRTY, THIRTY, BigDecimal.ZERO, true, ImmutableMap.<UUID, BigDecimal>of());
+ }
+
+ @Test(groups = "slow")
+ public void testPartialRefundWithInvoiceAdjustment() throws Exception {
+ verifyRefund(THIRTY, BigDecimal.TEN, BigDecimal.ZERO, true, ImmutableMap.<UUID, BigDecimal>of());
+ }
+
+ @Test(groups = "slow")
+ public void testFullRefundWithBothInvoiceItemAdjustments() throws Exception {
+ // Create an invoice with two items (30 \u20ac and 10 \u20ac)
+ final Invoice invoice = createAndPersistInvoice(invoiceSqlDao, invoiceItemSqlDao, clock,
+ ImmutableList.<BigDecimal>of(THIRTY, BigDecimal.TEN), CURRENCY, context);
+
+ // Fully adjust both items
+ final Map<UUID, BigDecimal> adjustments = new HashMap<UUID, BigDecimal>();
+ adjustments.put(invoice.getInvoiceItems().get(0).getId(), null);
+ adjustments.put(invoice.getInvoiceItems().get(1).getId(), null);
+
+ verifyRefund(invoice, new BigDecimal("40"), new BigDecimal("40"), BigDecimal.ZERO, true, adjustments);
+ }
+
+ @Test(groups = "slow")
+ public void testPartialRefundWithSingleInvoiceItemAdjustment() throws Exception {
+ // Create an invoice with two items (30 \u20ac and 10 \u20ac)
+ final Invoice invoice = createAndPersistInvoice(invoiceSqlDao, invoiceItemSqlDao, clock,
+ ImmutableList.<BigDecimal>of(THIRTY, BigDecimal.TEN), CURRENCY, context);
+
+ // Fully adjust both items
+ final Map<UUID, BigDecimal> adjustments = new HashMap<UUID, BigDecimal>();
+ adjustments.put(invoice.getInvoiceItems().get(0).getId(), null);
+
+ verifyRefund(invoice, new BigDecimal("40"), new BigDecimal("30"), BigDecimal.ZERO, true, adjustments);
+ }
+
+ @Test(groups = "slow")
+ public void testPartialRefundWithTwoInvoiceItemAdjustment() throws Exception {
+ // Create an invoice with two items (30 \u20ac and 10 \u20ac)
+ final Invoice invoice = createAndPersistInvoice(invoiceSqlDao, invoiceItemSqlDao, clock,
+ ImmutableList.<BigDecimal>of(THIRTY, BigDecimal.TEN), CURRENCY, context);
+ // Adjust partially both items: the invoice posted was 40 \u20ac, but we should really just have charged you 2 \u20ac
+ final ImmutableMap<UUID, BigDecimal> adjustments = ImmutableMap.<UUID, BigDecimal>of(invoice.getInvoiceItems().get(0).getId(), new BigDecimal("29"),
+ invoice.getInvoiceItems().get(1).getId(), new BigDecimal("9"));
+ verifyRefund(invoice, new BigDecimal("40"), new BigDecimal("38"), BigDecimal.ZERO, true, adjustments);
+ }
+
+ private void verifyRefund(final BigDecimal invoiceAmount, final BigDecimal refundAmount, final BigDecimal finalInvoiceAmount,
+ final boolean adjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts) throws InvoiceApiException {
+ final Invoice invoice = createAndPersistInvoice(invoiceSqlDao, invoiceItemSqlDao, clock, invoiceAmount, CURRENCY, context);
+ verifyRefund(invoice, invoiceAmount, refundAmount, finalInvoiceAmount, adjusted, invoiceItemIdsWithAmounts);
+ }
+
+ private void verifyRefund(final Invoice invoice, final BigDecimal invoiceAmount, final BigDecimal refundAmount, final BigDecimal finalInvoiceAmount,
+ final boolean adjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts) throws InvoiceApiException {
+ final InvoicePayment payment = createAndPersistPayment(invoicePaymentApi, clock, invoice.getId(), invoiceAmount, CURRENCY, context);
+
+ // Verify the initial invoice balance
+ final BigDecimal initialInvoiceBalance = invoicePaymentApi.getInvoice(invoice.getId()).getBalance();
+ Assert.assertEquals(initialInvoiceBalance.compareTo(BigDecimal.ZERO), 0);
+
+ // Create a full refund with no adjustment
+ final InvoicePayment refund = invoicePaymentApi.createRefund(payment.getPaymentId(), refundAmount, adjusted, invoiceItemIdsWithAmounts,
+ UUID.randomUUID(), context);
+ Assert.assertEquals(refund.getAmount().compareTo(refundAmount.negate()), 0);
+ Assert.assertEquals(refund.getCurrency(), CURRENCY);
+ Assert.assertEquals(refund.getInvoiceId(), invoice.getId());
+ Assert.assertEquals(refund.getPaymentId(), payment.getPaymentId());
+ Assert.assertEquals(refund.getType(), InvoicePaymentType.REFUND);
+
+ // Verify the current invoice balance
+ final BigDecimal newInvoiceBalance = invoicePaymentApi.getInvoice(invoice.getId()).getBalance().setScale(2, RoundingMode.HALF_UP);
+ Assert.assertEquals(newInvoiceBalance.compareTo(finalInvoiceAmount.setScale(2, RoundingMode.HALF_UP)), 0);
+ }
+}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/api/MockInvoicePaymentApi.java b/invoice/src/test/java/com/ning/billing/invoice/api/MockInvoicePaymentApi.java
index 87183b4..f229e4a 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/api/MockInvoicePaymentApi.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/api/MockInvoicePaymentApi.java
@@ -19,6 +19,7 @@ package com.ning.billing.invoice.api;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -167,8 +168,9 @@ public class MockInvoicePaymentApi implements InvoicePaymentApi {
}
@Override
- public InvoicePayment createRefund(UUID paymentId,
- BigDecimal amount, boolean isInvoiceAdjusted, UUID paymentCookieId, CallContext context)
+ public InvoicePayment createRefund(final UUID paymentId, final BigDecimal amount, final boolean isInvoiceAdjusted,
+ final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final UUID paymentCookieId,
+ final CallContext context)
throws InvoiceApiException {
return null;
}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/api/user/TestDefaultInvoiceUserApi.java b/invoice/src/test/java/com/ning/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
index 88ce414..2de15d7 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
@@ -136,7 +136,7 @@ public class TestDefaultInvoiceUserApi extends InvoiceApiTestBase {
// Adjust the invoice for the full amount
final InvoiceItem adjInvoiceItem = invoiceUserApi.insertInvoiceItemAdjustment(accountId, invoiceId, invoiceItem.getId(),
- clock.getUTCToday(), null, null, context);
+ clock.getUTCToday(), context);
Assert.assertEquals(adjInvoiceItem.getInvoiceId(), invoiceId);
Assert.assertEquals(adjInvoiceItem.getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
Assert.assertEquals(adjInvoiceItem.getAccountId(), accountId);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
index a483c06..301866c 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
@@ -25,12 +25,10 @@ import java.util.UUID;
import javax.annotation.Nullable;
-import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import com.google.inject.Inject;
-import com.ning.billing.account.api.BillCycleDay;
import com.ning.billing.catalog.api.Currency;
import com.ning.billing.invoice.api.Invoice;
import com.ning.billing.invoice.api.InvoiceApiException;
@@ -51,7 +49,7 @@ public class MockInvoiceDao implements InvoiceDao {
}
@Override
- public void create(final Invoice invoice, final int billCycleDay, final CallContext context) {
+ public void create(final Invoice invoice, final int billCycleDay, final boolean isRealInvoice, final CallContext context) {
synchronized (monitor) {
invoices.put(invoice.getId(), invoice);
}
@@ -282,10 +280,10 @@ public class MockInvoiceDao implements InvoiceDao {
}
@Override
- public InvoicePayment createRefund(UUID paymentId,
- BigDecimal amount, boolean isInvoiceAdjusted, UUID paymentCookieId, CallContext context)
+ public InvoicePayment createRefund(final UUID paymentId, final BigDecimal amount, final boolean isInvoiceAdjusted,
+ final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final UUID paymentCookieId,
+ final CallContext context)
throws InvoiceApiException {
return null;
}
-
}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDao.java
index 2bccd7b..9c8a92a 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDao.java
@@ -16,6 +16,7 @@
package com.ning.billing.invoice.dao;
+import java.math.BigDecimal;
import java.util.Map;
import java.util.UUID;
@@ -25,8 +26,11 @@ import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.ning.billing.ErrorCode;
import com.ning.billing.invoice.InvoiceTestSuite;
import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceApiException;
+import com.ning.billing.invoice.api.InvoicePayment;
import com.ning.billing.invoice.notification.NextBillingDatePoster;
import com.ning.billing.util.api.TagUserApi;
import com.ning.billing.util.callcontext.CallContext;
@@ -40,7 +44,10 @@ import com.ning.billing.util.tag.dao.MockTagDefinitionDao;
import com.ning.billing.util.tag.dao.TagDao;
import com.ning.billing.util.tag.dao.TagDefinitionDao;
+import com.google.common.collect.ImmutableMap;
+
public class TestDefaultInvoiceDao extends InvoiceTestSuite {
+
private InvoiceSqlDao invoiceSqlDao;
private TagUserApi tagUserApi;
private DefaultInvoiceDao dao;
@@ -59,6 +66,43 @@ public class TestDefaultInvoiceDao extends InvoiceTestSuite {
}
@Test(groups = "fast")
+ public void testComputePositiveRefundAmount() throws Exception {
+ // Verify the cases with no adjustment first
+ final Map<UUID, BigDecimal> noItemAdjustment = ImmutableMap.<UUID, BigDecimal>of();
+ verifyComputedRefundAmount(null, null, noItemAdjustment, BigDecimal.ZERO);
+ verifyComputedRefundAmount(null, BigDecimal.ZERO, noItemAdjustment, BigDecimal.ZERO);
+ verifyComputedRefundAmount(BigDecimal.TEN, null, noItemAdjustment, BigDecimal.TEN);
+ verifyComputedRefundAmount(BigDecimal.TEN, BigDecimal.ONE, noItemAdjustment, BigDecimal.ONE);
+ try {
+ verifyComputedRefundAmount(BigDecimal.ONE, BigDecimal.TEN, noItemAdjustment, BigDecimal.TEN);
+ Assert.fail("Shouldn't have been able to compute a refund amount");
+ } catch (InvoiceApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.REFUND_AMOUNT_TOO_HIGH.getCode());
+ }
+
+ // Try with adjustments now
+ final Map<UUID, BigDecimal> itemAdjustments = ImmutableMap.<UUID, BigDecimal>of(UUID.randomUUID(), BigDecimal.ONE,
+ UUID.randomUUID(), BigDecimal.TEN,
+ UUID.randomUUID(), BigDecimal.ZERO);
+ verifyComputedRefundAmount(new BigDecimal("100"), new BigDecimal("11"), itemAdjustments, new BigDecimal("11"));
+ try {
+ verifyComputedRefundAmount(new BigDecimal("100"), BigDecimal.TEN, itemAdjustments, BigDecimal.TEN);
+ Assert.fail("Shouldn't have been able to compute a refund amount");
+ } catch (InvoiceApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.REFUND_AMOUNT_DONT_MATCH_ITEMS_TO_ADJUST.getCode());
+ }
+ }
+
+ private void verifyComputedRefundAmount(final BigDecimal paymentAmount, final BigDecimal requestedAmount,
+ final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final BigDecimal expectedRefundAmount) throws InvoiceApiException {
+ final InvoicePayment invoicePayment = Mockito.mock(InvoicePayment.class);
+ Mockito.when(invoicePayment.getAmount()).thenReturn(paymentAmount);
+
+ final BigDecimal actualRefundAmount = dao.computePositiveRefundAmount(invoicePayment, requestedAmount, invoiceItemIdsWithAmounts);
+ Assert.assertEquals(actualRefundAmount, expectedRefundAmount);
+ }
+
+ @Test(groups = "fast")
public void testFindByNumber() throws Exception {
final Integer number = Integer.MAX_VALUE;
final Invoice invoice = Mockito.mock(Invoice.class);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
index 01e628d..55a2e61 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
@@ -27,7 +27,6 @@ import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.mockito.Mockito;
-import org.testng.Assert;
import org.testng.annotations.Test;
import com.ning.billing.catalog.DefaultPrice;
@@ -66,6 +65,8 @@ import com.ning.billing.util.tag.Tag;
import com.ning.billing.util.tag.dao.AuditedTagDao;
import com.ning.billing.util.tag.dao.TagDao;
+import com.google.common.collect.ImmutableMap;
+
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
@@ -79,7 +80,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
final Invoice invoice = new DefaultInvoice(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
final LocalDate invoiceDate = invoice.getInvoiceDate();
- invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), true, context);
final List<Invoice> invoices = invoiceDao.getInvoicesByAccount(accountId);
assertNotNull(invoices);
@@ -105,7 +106,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
new BigDecimal("21.00"), new BigDecimal("7.00"), Currency.USD);
invoice.addInvoiceItem(invoiceItem);
- invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), true, context);
final Invoice savedInvoice = invoiceDao.getById(invoiceId);
assertNotNull(savedInvoice);
@@ -150,7 +151,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
// Create invoice 1 (subscriptions 1-4)
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final UUID invoiceId1 = invoice1.getId();
@@ -175,7 +176,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
// Create invoice 2 (subscriptions 1-3)
final DefaultInvoice invoice2 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate, Currency.USD);
- invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), true, context);
final UUID invoiceId2 = invoice2.getId();
@@ -226,7 +227,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
// Create invoice 1 (subscriptions 1-4)
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final UUID invoiceId1 = invoice1.getId();
@@ -251,7 +252,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
// create invoice 2 (subscriptions 1-3)
final DefaultInvoice invoice2 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate, Currency.USD);
- invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), true, context);
final UUID invoiceId2 = invoice2.getId();
@@ -302,7 +303,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
// Create invoice 1 (subscriptions 1-4)
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final UUID invoiceId1 = invoice1.getId();
@@ -343,7 +344,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
// create invoice 2 (subscriptions 1-3)
final DefaultInvoice invoice2 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate, Currency.USD);
- invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), true, context);
final UUID invoiceId2 = invoice2.getId();
@@ -392,11 +393,11 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
final UUID accountId = UUID.randomUUID();
final LocalDate targetDate1 = new LocalDate(2011, 10, 6);
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final LocalDate targetDate2 = new LocalDate(2011, 12, 6);
final Invoice invoice2 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate2, Currency.USD);
- invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), true, context);
List<Invoice> invoices;
invoices = invoiceDao.getInvoicesByAccount(accountId, new LocalDate(2011, 1, 1));
@@ -421,7 +422,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
final UUID bundleId = UUID.randomUUID();
final LocalDate targetDate1 = new LocalDate(2011, 10, 6);
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final LocalDate startDate = new LocalDate(2011, 3, 1);
final LocalDate endDate = startDate.plusMonths(1);
@@ -451,7 +452,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
final UUID bundleId = UUID.randomUUID();
final LocalDate targetDate1 = new LocalDate(2011, 10, 6);
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final LocalDate startDate = new LocalDate(2011, 3, 1);
final LocalDate endDate = startDate.plusMonths(1);
@@ -475,7 +476,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
final UUID bundleId = UUID.randomUUID();
final LocalDate targetDate1 = new LocalDate(2011, 10, 6);
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final LocalDate startDate = new LocalDate(2011, 3, 1);
final LocalDate endDate = startDate.plusMonths(1);
@@ -500,7 +501,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
final UUID accountId = UUID.randomUUID();
final LocalDate targetDate1 = new LocalDate(2011, 10, 6);
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final BigDecimal payment1 = new BigDecimal("48.0");
final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD);
@@ -526,7 +527,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
final UUID bundleId = UUID.randomUUID();
final LocalDate targetDate1 = new LocalDate(2011, 10, 6);
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final LocalDate startDate = new LocalDate(2011, 3, 1);
final LocalDate endDate = startDate.plusMonths(1);
@@ -550,7 +551,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
balance = invoiceDao.getAccountBalance(accountId);
assertEquals(balance.compareTo(new BigDecimal("0.00")), 0);
- invoiceDao.createRefund(paymentId, refund1, withAdjustment, UUID.randomUUID(), context);
+ invoiceDao.createRefund(paymentId, refund1, withAdjustment, ImmutableMap.<UUID, BigDecimal>of(), UUID.randomUUID(), context);
balance = invoiceDao.getAccountBalance(accountId);
if (withAdjustment) {
assertEquals(balance.compareTo(BigDecimal.ZERO), 0);
@@ -592,7 +593,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
final UUID bundleId = UUID.randomUUID();
final LocalDate targetDate1 = new LocalDate(2011, 10, 6);
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final LocalDate startDate = new LocalDate(2011, 3, 1);
final LocalDate endDate = startDate.plusMonths(1);
@@ -642,7 +643,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
assertEquals(cba.compareTo(new BigDecimal("10.00")), 0);
// PARTIAL REFUND on the payment
- invoiceDao.createRefund(paymentId, refundAmount, withAdjustment, UUID.randomUUID(), context);
+ invoiceDao.createRefund(paymentId, refundAmount, withAdjustment, ImmutableMap.<UUID, BigDecimal>of(), UUID.randomUUID(), context);
balance = invoiceDao.getAccountBalance(accountId);
assertEquals(balance.compareTo(expectedFinalBalance), 0);
@@ -659,7 +660,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
final UUID bundleId = UUID.randomUUID();
final LocalDate targetDate1 = new LocalDate(2011, 10, 6);
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final LocalDate startDate = new LocalDate(2011, 3, 1);
final LocalDate endDate = startDate.plusMonths(1);
@@ -721,7 +722,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
// NEXT RECURRING on invoice 2
final Invoice invoice2 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1.plusMonths(1), Currency.USD);
- invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), true, context);
final RecurringInvoiceItem nextItem = new RecurringInvoiceItem(invoice2.getId(), accountId, bundleId, UUID.randomUUID(), "test plan", "test bla", startDate.plusMonths(1),
endDate.plusMonths(1), rate2, rate2, Currency.USD);
@@ -809,7 +810,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
// Crete one invoice with a fixed invoice item
final LocalDate targetDate = new LocalDate(2011, 2, 15);
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final LocalDate startDate = new LocalDate(2011, 3, 1);
@@ -852,7 +853,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
final UUID bundleId = UUID.randomUUID();
final LocalDate targetDate1 = new LocalDate(2011, 10, 6);
final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
final LocalDate startDate = new LocalDate(2011, 3, 1);
final LocalDate endDate = startDate.plusMonths(1);
@@ -881,7 +882,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
final LocalDate targetDate2 = new LocalDate(2011, 7, 1);
final Invoice invoice2 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate2, Currency.USD);
- invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), true, context);
final LocalDate startDate2 = new LocalDate(2011, 6, 1);
final LocalDate endDate2 = startDate2.plusMonths(3);
@@ -951,8 +952,8 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
assertEquals(invoice2.getBalance(), FIVE);
invoiceList.add(invoice2);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
- invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
+ invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), true, context);
final Invoice savedInvoice1 = invoiceDao.getById(invoice1.getId());
assertEquals(savedInvoice1.getBalance(), ZERO);
@@ -1094,7 +1095,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
assertEquals(invoice.getNumberOfItems(), 2);
assertEquals(invoice.getBalance().compareTo(cheapAmount), 0);
- invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), true, context);
final Invoice savedInvoice = invoiceDao.getById(invoice.getId());
assertNotNull(savedInvoice);
@@ -1130,7 +1131,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
Invoice invoice1 = generator.generateInvoice(UUID.randomUUID(), events, invoices, new LocalDate(targetDate1), DateTimeZone.UTC, Currency.USD);
invoices.add(invoice1);
- invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
invoice1 = invoiceDao.getById(invoice1.getId());
assertNotNull(invoice1.getInvoiceNumber());
@@ -1140,7 +1141,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
"testEvent2", 2L, SubscriptionTransitionType.CHANGE);
events.add(event2);
Invoice invoice2 = generator.generateInvoice(UUID.randomUUID(), events, invoices, new LocalDate(targetDate2), DateTimeZone.UTC, Currency.USD);
- invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), true, context);
invoice2 = invoiceDao.getById(invoice2.getId());
assertNotNull(invoice2.getInvoiceNumber());
}
@@ -1167,7 +1168,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
events.add(event1);
final Invoice invoice = generator.generateInvoice(UUID.randomUUID(), events, null, new LocalDate(targetDate1), DateTimeZone.UTC, Currency.USD);
- invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), true, context);
invoiceDao.setWrittenOff(invoice.getId(), context);
final TagDao tagDao = new AuditedTagDao(dbi, tagEventBuilder, bus);
@@ -1198,7 +1199,7 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
events.add(event1);
final Invoice invoice = generator.generateInvoice(UUID.randomUUID(), events, null, new LocalDate(targetDate1), DateTimeZone.UTC, Currency.USD);
- invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), context);
+ invoiceDao.create(invoice, invoice.getTargetDate().getDayOfMonth(), true, context);
invoiceDao.setWrittenOff(invoice.getId(), context);
final TagDao tagDao = new AuditedTagDao(dbi, tagEventBuilder, bus);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java
index 5e82f30..e76e890 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java
@@ -63,7 +63,7 @@ public class TestInvoiceDaoForItemAdjustment extends InvoiceDaoTestBase {
new LocalDate(2010, 1, 1), new LocalDate(2010, 4, 1),
INVOICE_ITEM_AMOUNT, new BigDecimal("7.00"), Currency.USD);
invoice.addInvoiceItem(invoiceItem);
- invoiceDao.create(invoice, 1, context);
+ invoiceDao.create(invoice, 1, true, context);
try {
invoiceDao.insertInvoiceItemAdjustment(invoice.getAccountId(), UUID.randomUUID(), invoiceItem.getId(), new LocalDate(2010, 1, 1), null, null, context);
@@ -81,7 +81,7 @@ public class TestInvoiceDaoForItemAdjustment extends InvoiceDaoTestBase {
new LocalDate(2010, 1, 1), new LocalDate(2010, 4, 1),
INVOICE_ITEM_AMOUNT, new BigDecimal("7.00"), Currency.USD);
invoice.addInvoiceItem(invoiceItem);
- invoiceDao.create(invoice, 1, context);
+ invoiceDao.create(invoice, 1, true, context);
final InvoiceItem adjustedInvoiceItem = createAndCheckAdjustment(invoice, invoiceItem, null);
Assert.assertEquals(adjustedInvoiceItem.getAmount().compareTo(invoiceItem.getAmount().negate()), 0);
@@ -95,7 +95,7 @@ public class TestInvoiceDaoForItemAdjustment extends InvoiceDaoTestBase {
new LocalDate(2010, 1, 1), new LocalDate(2010, 4, 1),
INVOICE_ITEM_AMOUNT, new BigDecimal("7.00"), Currency.USD);
invoice.addInvoiceItem(invoiceItem);
- invoiceDao.create(invoice, 1, context);
+ invoiceDao.create(invoice, 1, true, context);
final InvoiceItem adjustedInvoiceItem = createAndCheckAdjustment(invoice, invoiceItem, BigDecimal.TEN);
Assert.assertEquals(adjustedInvoiceItem.getAmount().compareTo(BigDecimal.TEN.negate()), 0);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceItemDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceItemDao.java
index f8766f9..4c98b52 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceItemDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceItemDao.java
@@ -115,7 +115,7 @@ public class TestInvoiceItemDao extends InvoiceDaoTestBase {
final LocalDate targetDate = new LocalDate(2011, 5, 23);
final DefaultInvoice invoice = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate, Currency.USD);
- invoiceDao.create(invoice, targetDate.getDayOfMonth(), context);
+ invoiceDao.create(invoice, targetDate.getDayOfMonth(), true, context);
final UUID invoiceId = invoice.getId();
final LocalDate startDate = new LocalDate(2011, 3, 1);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/model/TestInAdvanceBillingMode.java b/invoice/src/test/java/com/ning/billing/invoice/model/TestInAdvanceBillingMode.java
index 10ded27..fccd7b1 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/model/TestInAdvanceBillingMode.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/model/TestInAdvanceBillingMode.java
@@ -41,8 +41,6 @@ public class TestInAdvanceBillingMode {
final int billingCycleDayLocal = 15;
final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
- expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 15));
-
verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tests/InvoiceTestUtils.java b/invoice/src/test/java/com/ning/billing/invoice/tests/InvoiceTestUtils.java
new file mode 100644
index 0000000..8826134
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/tests/InvoiceTestUtils.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.invoice.tests;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.mockito.Mockito;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoicePayment;
+import com.ning.billing.invoice.api.InvoicePayment.InvoicePaymentType;
+import com.ning.billing.invoice.api.InvoicePaymentApi;
+import com.ning.billing.invoice.dao.InvoiceItemSqlDao;
+import com.ning.billing.invoice.dao.InvoiceSqlDao;
+import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.clock.Clock;
+
+import com.google.common.collect.ImmutableList;
+
+public class InvoiceTestUtils {
+
+ private InvoiceTestUtils() {}
+
+ public static Invoice createAndPersistInvoice(final InvoiceSqlDao invoiceSqlDao,
+ final InvoiceItemSqlDao invoiceItemSqlDao,
+ final Clock clock,
+ final BigDecimal amount,
+ final Currency currency,
+ final CallContext callContext) {
+ return createAndPersistInvoice(invoiceSqlDao, invoiceItemSqlDao, clock, ImmutableList.<BigDecimal>of(amount),
+ currency, callContext);
+ }
+
+ public static Invoice createAndPersistInvoice(final InvoiceSqlDao invoiceSqlDao,
+ final InvoiceItemSqlDao invoiceItemSqlDao,
+ final Clock clock,
+ final List<BigDecimal> amounts,
+ final Currency currency,
+ final CallContext callContext) {
+ final Invoice invoice = Mockito.mock(Invoice.class);
+ final UUID invoiceId = UUID.randomUUID();
+ final UUID accountId = UUID.randomUUID();
+
+ Mockito.when(invoice.getId()).thenReturn(invoiceId);
+ Mockito.when(invoice.getAccountId()).thenReturn(accountId);
+ Mockito.when(invoice.getInvoiceDate()).thenReturn(clock.getUTCToday());
+ Mockito.when(invoice.getTargetDate()).thenReturn(clock.getUTCToday());
+ Mockito.when(invoice.getCurrency()).thenReturn(currency);
+ Mockito.when(invoice.isMigrationInvoice()).thenReturn(false);
+
+ final List<InvoiceItem> invoiceItems = new ArrayList<InvoiceItem>();
+ for (final BigDecimal amount : amounts) {
+ final InvoiceItem invoiceItem = createInvoiceItem(clock, invoiceId, accountId, amount, currency);
+ invoiceItemSqlDao.create(invoiceItem, callContext);
+ invoiceItems.add(invoiceItem);
+ }
+ Mockito.when(invoice.getInvoiceItems()).thenReturn(invoiceItems);
+
+ invoiceSqlDao.create(invoice, callContext);
+
+ return invoice;
+ }
+
+ public static InvoiceItem createInvoiceItem(final Clock clock, final UUID invoiceId, final UUID accountId, final BigDecimal amount, final Currency currency) {
+ return new FixedPriceInvoiceItem(invoiceId, accountId, UUID.randomUUID(), UUID.randomUUID(),
+ "charge back test", "charge back phase", clock.getUTCToday(), amount, currency);
+ }
+
+ public static InvoicePayment createAndPersistPayment(final InvoicePaymentApi invoicePaymentApi,
+ final Clock clock,
+ final UUID invoiceId,
+ final BigDecimal amount,
+ final Currency currency,
+ final CallContext callContext) {
+ final InvoicePayment payment = Mockito.mock(InvoicePayment.class);
+ Mockito.when(payment.getId()).thenReturn(UUID.randomUUID());
+ Mockito.when(payment.getType()).thenReturn(InvoicePaymentType.ATTEMPT);
+ Mockito.when(payment.getInvoiceId()).thenReturn(invoiceId);
+ Mockito.when(payment.getPaymentId()).thenReturn(UUID.randomUUID());
+ Mockito.when(payment.getPaymentCookieId()).thenReturn(UUID.randomUUID());
+ Mockito.when(payment.getPaymentDate()).thenReturn(clock.getUTCNow());
+ Mockito.when(payment.getAmount()).thenReturn(amount);
+ Mockito.when(payment.getCurrency()).thenReturn(currency);
+
+ invoicePaymentApi.notifyOfPayment(payment, callContext);
+
+ return payment;
+ }
+}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java b/invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java
index ef43230..689b11a 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java
@@ -19,11 +19,9 @@ package com.ning.billing.invoice.tests;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URL;
-import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
-import org.mockito.Mockito;
import org.skife.jdbi.v2.IDBI;
import org.skife.jdbi.v2.exceptions.TransactionFailedException;
import org.testng.annotations.BeforeSuite;
@@ -35,16 +33,14 @@ import com.ning.billing.dbi.MysqlTestingHelper;
import com.ning.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
import com.ning.billing.invoice.api.Invoice;
import com.ning.billing.invoice.api.InvoiceApiException;
-import com.ning.billing.invoice.api.InvoiceItem;
import com.ning.billing.invoice.api.InvoicePayment;
-import com.ning.billing.invoice.api.InvoicePayment.InvoicePaymentType;
import com.ning.billing.invoice.api.InvoicePaymentApi;
import com.ning.billing.invoice.api.invoice.DefaultInvoicePaymentApi;
import com.ning.billing.invoice.dao.DefaultInvoiceDao;
import com.ning.billing.invoice.dao.InvoiceDao;
+import com.ning.billing.invoice.dao.InvoiceItemSqlDao;
import com.ning.billing.invoice.dao.InvoiceSqlDao;
import com.ning.billing.invoice.glue.InvoiceModuleWithEmbeddedDb;
-import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
import com.ning.billing.invoice.notification.MockNextBillingDatePoster;
import com.ning.billing.invoice.notification.NextBillingDatePoster;
import com.ning.billing.util.api.TagUserApi;
@@ -58,16 +54,20 @@ import com.ning.billing.util.tag.dao.MockTagDefinitionDao;
import com.ning.billing.util.tag.dao.TagDao;
import com.ning.billing.util.tag.dao.TagDefinitionDao;
+import static com.ning.billing.invoice.tests.InvoiceTestUtils.createAndPersistInvoice;
+import static com.ning.billing.invoice.tests.InvoiceTestUtils.createAndPersistPayment;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
+
private static final BigDecimal FIFTEEN = new BigDecimal("15.00");
private static final BigDecimal THIRTY = new BigDecimal("30.00");
private static final BigDecimal ONE_MILLION = new BigDecimal("1000000.00");
private InvoiceSqlDao invoiceSqlDao;
+ private InvoiceItemSqlDao invoiceItemSqlDao;
private InvoicePaymentApi invoicePaymentApi;
private CallContext context;
private final Clock clock = new ClockMock();
@@ -83,6 +83,8 @@ public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
invoiceSqlDao = dbi.onDemand(InvoiceSqlDao.class);
invoiceSqlDao.test();
+ invoiceItemSqlDao = dbi.onDemand(InvoiceItemSqlDao.class);
+ invoiceItemSqlDao.test();
final NextBillingDatePoster nextBillingDatePoster = new MockNextBillingDatePoster();
final TagDefinitionDao tagDefinitionDao = new MockTagDefinitionDao();
final TagDao tagDao = new MockTagDao();
@@ -105,8 +107,8 @@ public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
@Test(groups = "slow")
public void testCompleteChargeBack() throws InvoiceApiException {
- final Invoice invoice = createAndPersistInvoice(THIRTY);
- final InvoicePayment payment = createAndPersistPayment(invoice.getId(), THIRTY);
+ final Invoice invoice = createAndPersistInvoice(invoiceSqlDao, invoiceItemSqlDao, clock, THIRTY, CURRENCY, context);
+ final InvoicePayment payment = createAndPersistPayment(invoicePaymentApi, clock, invoice.getId(), THIRTY, CURRENCY, context);
// create a full charge back
invoicePaymentApi.createChargeback(payment.getId(), THIRTY, context);
@@ -118,8 +120,8 @@ public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
@Test(groups = "slow")
public void testPartialChargeBack() throws InvoiceApiException {
- final Invoice invoice = createAndPersistInvoice(THIRTY);
- final InvoicePayment payment = createAndPersistPayment(invoice.getId(), THIRTY);
+ final Invoice invoice = createAndPersistInvoice(invoiceSqlDao, invoiceItemSqlDao, clock, THIRTY, CURRENCY, context);
+ final InvoicePayment payment = createAndPersistPayment(invoicePaymentApi, clock, invoice.getId(), THIRTY, CURRENCY, context);
// create a partial charge back
invoicePaymentApi.createChargeback(payment.getId(), FIFTEEN, context);
@@ -132,8 +134,8 @@ public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
@Test(groups = "slow", expectedExceptions = InvoiceApiException.class)
public void testChargeBackLargerThanPaymentAmount() throws InvoiceApiException {
try {
- final Invoice invoice = createAndPersistInvoice(THIRTY);
- final InvoicePayment payment = createAndPersistPayment(invoice.getId(), THIRTY);
+ final Invoice invoice = createAndPersistInvoice(invoiceSqlDao, invoiceItemSqlDao, clock, THIRTY, CURRENCY, context);
+ final InvoicePayment payment = createAndPersistPayment(invoicePaymentApi, clock, invoice.getId(), THIRTY, CURRENCY, context);
// create a large charge back
invoicePaymentApi.createChargeback(payment.getId(), ONE_MILLION, context);
@@ -146,8 +148,8 @@ public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
@Test(groups = "slow", expectedExceptions = InvoiceApiException.class)
public void testNegativeChargeBackAmount() throws InvoiceApiException {
try {
- final Invoice invoice = createAndPersistInvoice(THIRTY);
- final InvoicePayment payment = createAndPersistPayment(invoice.getId(), THIRTY);
+ final Invoice invoice = createAndPersistInvoice(invoiceSqlDao, invoiceItemSqlDao, clock, THIRTY, CURRENCY, context);
+ final InvoicePayment payment = createAndPersistPayment(invoicePaymentApi, clock, invoice.getId(), THIRTY, CURRENCY, context);
// create a partial charge back
invoicePaymentApi.createChargeback(payment.getId(), BigDecimal.ONE.negate(), context);
@@ -158,8 +160,8 @@ public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
@Test(groups = "slow")
public void testGetAccountIdFromPaymentIdHappyPath() throws InvoiceApiException {
- final Invoice invoice = createAndPersistInvoice(THIRTY);
- final InvoicePayment payment = createAndPersistPayment(invoice.getId(), THIRTY);
+ final Invoice invoice = createAndPersistInvoice(invoiceSqlDao, invoiceItemSqlDao, clock, THIRTY, CURRENCY, context);
+ final InvoicePayment payment = createAndPersistPayment(invoicePaymentApi, clock, invoice.getId(), THIRTY, CURRENCY, context);
final UUID accountId = invoicePaymentApi.getAccountIdFromInvoicePaymentId(payment.getId());
assertEquals(accountId, invoice.getAccountId());
}
@@ -178,8 +180,8 @@ public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
@Test(groups = "slow")
public void testGetChargeBacksByAccountIdHappyPath() throws InvoiceApiException {
- final Invoice invoice = createAndPersistInvoice(THIRTY);
- final InvoicePayment payment = createAndPersistPayment(invoice.getId(), THIRTY);
+ final Invoice invoice = createAndPersistInvoice(invoiceSqlDao, invoiceItemSqlDao, clock, THIRTY, CURRENCY, context);
+ final InvoicePayment payment = createAndPersistPayment(invoicePaymentApi, clock, invoice.getId(), THIRTY, CURRENCY, context);
// create a partial charge back
invoicePaymentApi.createChargeback(payment.getId(), FIFTEEN, context);
@@ -199,8 +201,8 @@ public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
@Test(groups = "slow")
public void testGetChargeBacksByInvoicePaymentIdHappyPath() throws InvoiceApiException {
- final Invoice invoice = createAndPersistInvoice(THIRTY);
- final InvoicePayment payment = createAndPersistPayment(invoice.getId(), THIRTY);
+ final Invoice invoice = createAndPersistInvoice(invoiceSqlDao, invoiceItemSqlDao, clock, THIRTY, CURRENCY, context);
+ final InvoicePayment payment = createAndPersistPayment(invoicePaymentApi, clock, invoice.getId(), THIRTY, CURRENCY, context);
// create a partial charge back
invoicePaymentApi.createChargeback(payment.getId(), FIFTEEN, context);
@@ -210,45 +212,4 @@ public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
assertEquals(chargebacks.size(), 1);
assertEquals(chargebacks.get(0).getLinkedInvoicePaymentId(), payment.getId());
}
-
- private Invoice createAndPersistInvoice(final BigDecimal amount) {
- final Invoice invoice = Mockito.mock(Invoice.class);
- final UUID invoiceId = UUID.randomUUID();
- final UUID accountId = UUID.randomUUID();
-
- Mockito.when(invoice.getId()).thenReturn(invoiceId);
- Mockito.when(invoice.getAccountId()).thenReturn(accountId);
- Mockito.when(invoice.getInvoiceDate()).thenReturn(clock.getUTCToday());
- Mockito.when(invoice.getTargetDate()).thenReturn(clock.getUTCToday());
- Mockito.when(invoice.getCurrency()).thenReturn(CURRENCY);
- Mockito.when(invoice.isMigrationInvoice()).thenReturn(false);
-
- final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
- items.add(createInvoiceItem(invoiceId, accountId, amount));
- Mockito.when(invoice.getInvoiceItems()).thenReturn(items);
-
- invoiceSqlDao.create(invoice, context);
-
- return invoice;
- }
-
- private InvoiceItem createInvoiceItem(final UUID invoiceId, final UUID accountId, final BigDecimal amount) {
- return new FixedPriceInvoiceItem(invoiceId, accountId, UUID.randomUUID(), UUID.randomUUID(),
- "charge back test", "charge back phase", clock.getUTCToday(), amount, CURRENCY);
- }
-
- private InvoicePayment createAndPersistPayment(final UUID invoiceId, final BigDecimal amount) {
- final InvoicePayment payment = Mockito.mock(InvoicePayment.class);
- Mockito.when(payment.getId()).thenReturn(UUID.randomUUID());
- Mockito.when(payment.getType()).thenReturn(InvoicePaymentType.ATTEMPT);
- Mockito.when(payment.getInvoiceId()).thenReturn(invoiceId);
- Mockito.when(payment.getPaymentId()).thenReturn(UUID.randomUUID());
- Mockito.when(payment.getPaymentDate()).thenReturn(clock.getUTCNow());
- Mockito.when(payment.getAmount()).thenReturn(amount);
- Mockito.when(payment.getCurrency()).thenReturn(CURRENCY);
-
- invoicePaymentApi.notifyOfPayment(payment, context);
-
- return payment;
- }
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountJson.java
index 736fba2..a8e346c 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountJson.java
@@ -188,25 +188,6 @@ public class AccountJson extends AccountJsonSimple {
};
}
- // Seems like Jackson (JacksonJsonProvider.readFrom(Class<Object>, Type, Annotation[], MediaType, MultivaluedMap<String,String>, InputStream) line: 443)
- // needs us to define a default CTOR to instantiate the class first.
- public AccountJson() {
- super();
- this.name = null;
- this.length = null;
- this.email = null;
- this.billCycleDayJson = null;
- this.currency = null;
- this.paymentMethodId = null;
- this.timeZone = null;
- this.address1 = null;
- this.address2 = null;
- this.company = null;
- this.state = null;
- this.country = null;
- this.phone = null;
- }
-
@JsonCreator
public AccountJson(@JsonProperty("accountId") final String accountId,
@JsonProperty("name") final String name,
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountJsonSimple.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountJsonSimple.java
index d1755cd..40d17ae 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountJsonSimple.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountJsonSimple.java
@@ -24,11 +24,6 @@ public class AccountJsonSimple {
protected final String externalKey;
- public AccountJsonSimple() {
- this.accountId = null;
- this.externalKey = null;
- }
-
@JsonCreator
public AccountJsonSimple(@JsonProperty("accountId") final String accountId,
@JsonProperty("externalKey") final String externalKey) {
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java
index 29b1d29..9d889d4 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java
@@ -125,7 +125,8 @@ public class AccountTimelineJson {
final List<RefundJson> refunds = new ArrayList<RefundJson>();
for (final Refund refund : refundsByPayment.get(payment.getId())) {
final List<AuditLog> auditLogs = refundsAuditLogs.get(refund.getId());
- refunds.add(new RefundJson(refund, auditLogs));
+ // TODO add adjusted invoice items?
+ refunds.add(new RefundJson(refund, null, auditLogs));
}
final List<ChargebackJson> chargebacks = new ArrayList<ChargebackJson>();
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditCollectionJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditCollectionJson.java
index 2840e4d..5bb99dc 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditCollectionJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditCollectionJson.java
@@ -23,17 +23,17 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class CreditCollectionJson {
- private final UUID accountId;
+ private final String accountId;
private final List<CreditJson> credits;
@JsonCreator
- public CreditCollectionJson(@JsonProperty("accountId") final UUID accountId,
+ public CreditCollectionJson(@JsonProperty("accountId") final String accountId,
@JsonProperty("credits") final List<CreditJson> credits) {
this.accountId = accountId;
this.credits = credits;
}
- public UUID getAccountId() {
+ public String getAccountId() {
return accountId;
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java
index 458e277..e8c8f44 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java
@@ -18,7 +18,6 @@ package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
import java.util.List;
-import java.util.UUID;
import javax.annotation.Nullable;
@@ -34,21 +33,21 @@ import com.fasterxml.jackson.annotation.JsonProperty;
public class CreditJson extends JsonBase {
private final BigDecimal creditAmount;
- private final UUID invoiceId;
+ private final String invoiceId;
private final String invoiceNumber;
private final DateTime requestedDate;
private final DateTime effectiveDate;
private final String reason;
- private final UUID accountId;
+ private final String accountId;
@JsonCreator
public CreditJson(@JsonProperty("creditAmount") final BigDecimal creditAmount,
- @JsonProperty("invoiceId") final UUID invoiceId,
+ @JsonProperty("invoiceId") final String invoiceId,
@JsonProperty("invoiceNumber") final String invoiceNumber,
@JsonProperty("requestedDate") final DateTime requestedDate,
@JsonProperty("effectiveDate") final DateTime effectiveDate,
@JsonProperty("reason") final String reason,
- @JsonProperty("accountId") final UUID accountId,
+ @JsonProperty("accountId") final String accountId,
@JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
super(auditLogs);
this.creditAmount = creditAmount;
@@ -63,12 +62,12 @@ public class CreditJson extends JsonBase {
public CreditJson(final InvoiceItem credit, final DateTimeZone accountTimeZone, final List<AuditLog> auditLogs) {
super(toAuditLogJson(auditLogs));
this.creditAmount = credit.getAmount();
- this.invoiceId = credit.getInvoiceId();
+ this.invoiceId = toString(credit.getInvoiceId());
this.invoiceNumber = null;
this.requestedDate = null;
this.effectiveDate = credit.getStartDate().toDateTimeAtStartOfDay(accountTimeZone);
this.reason = null;
- this.accountId = credit.getAccountId();
+ this.accountId = toString(credit.getAccountId());
}
public CreditJson(final InvoiceItem credit, final DateTimeZone timeZone) {
@@ -79,7 +78,7 @@ public class CreditJson extends JsonBase {
return creditAmount;
}
- public UUID getInvoiceId() {
+ public String getInvoiceId() {
return invoiceId;
}
@@ -99,7 +98,7 @@ public class CreditJson extends JsonBase {
return reason;
}
- public UUID getAccountId() {
+ public String getAccountId() {
return accountId;
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java
index 390837b..b2e1e58 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java
@@ -23,11 +23,15 @@ import javax.annotation.Nullable;
import org.joda.time.DateTime;
+import com.ning.billing.invoice.api.InvoiceItem;
import com.ning.billing.payment.api.Refund;
import com.ning.billing.util.audit.AuditLog;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
public class RefundJson extends JsonBase {
@@ -37,6 +41,7 @@ public class RefundJson extends JsonBase {
private final Boolean isAdjusted;
private final DateTime requestedDate;
private final DateTime effectiveDate;
+ private final List<InvoiceItemJsonSimple> adjustments;
@JsonCreator
public RefundJson(@JsonProperty("refund_id") final String refundId,
@@ -45,6 +50,7 @@ public class RefundJson extends JsonBase {
@JsonProperty("adjusted") final Boolean isAdjusted,
@JsonProperty("requestedDate") final DateTime requestedDate,
@JsonProperty("effectiveDate") final DateTime effectiveDate,
+ @JsonProperty("adjustments") @Nullable final List<InvoiceItemJsonSimple> adjustments,
@JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
super(auditLogs);
this.refundId = refundId;
@@ -53,15 +59,19 @@ public class RefundJson extends JsonBase {
this.isAdjusted = isAdjusted;
this.requestedDate = requestedDate;
this.effectiveDate = effectiveDate;
+ this.adjustments = adjustments;
}
- public RefundJson(final Refund refund) {
- this(refund, null);
- }
-
- public RefundJson(final Refund refund, final List<AuditLog> auditLogs) {
+ public RefundJson(final Refund refund, @Nullable final List<InvoiceItem> adjustments, @Nullable final List<AuditLog> auditLogs) {
this(refund.getId().toString(), refund.getPaymentId().toString(), refund.getRefundAmount(), refund.isAdjusted(),
- refund.getEffectiveDate(), refund.getEffectiveDate(), toAuditLogJson(auditLogs));
+ refund.getEffectiveDate(), refund.getEffectiveDate(),
+ adjustments == null ? null : ImmutableList.<InvoiceItemJsonSimple>copyOf(Collections2.transform(adjustments, new Function<InvoiceItem, InvoiceItemJsonSimple>() {
+ @Override
+ public InvoiceItemJsonSimple apply(@Nullable final InvoiceItem input) {
+ return new InvoiceItemJsonSimple(input);
+ }
+ })),
+ toAuditLogJson(auditLogs));
}
public String getRefundId() {
@@ -88,6 +98,10 @@ public class RefundJson extends JsonBase {
return effectiveDate;
}
+ public List<InvoiceItemJsonSimple> getAdjustments() {
+ return adjustments;
+ }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
@@ -98,6 +112,7 @@ public class RefundJson extends JsonBase {
sb.append(", isAdjusted=").append(isAdjusted);
sb.append(", requestedDate=").append(requestedDate);
sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append(", adjustments=").append(adjustments);
sb.append('}');
return sb.toString();
}
@@ -110,6 +125,7 @@ public class RefundJson extends JsonBase {
result = 31 * result + (isAdjusted != null ? isAdjusted.hashCode() : 0);
result = 31 * result + (requestedDate != null ? requestedDate.hashCode() : 0);
result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ result = 31 * result + (adjustments != null ? adjustments.hashCode() : 0);
return result;
}
@@ -185,6 +201,14 @@ public class RefundJson extends JsonBase {
return false;
}
+ if (adjustments == null) {
+ if (other.adjustments != null) {
+ return false;
+ }
+ } else if (!adjustments.equals(other.adjustments)) {
+ return false;
+ }
+
return true;
}
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/InvoiceApiExceptionMapper.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/InvoiceApiExceptionMapper.java
index 85c3bb2..01c3bda 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/InvoiceApiExceptionMapper.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/InvoiceApiExceptionMapper.java
@@ -58,6 +58,10 @@ public class InvoiceApiExceptionMapper extends ExceptionMapperBase implements Ex
return buildNotFoundResponse(exception, uriInfo);
} else if (exception.getCode() == ErrorCode.INVOICE_TARGET_DATE_TOO_FAR_IN_THE_FUTURE.getCode()) {
return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.CREDIT_AMOUNT_INVALID.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_ITEM_ADJUSTMENT_AMOUNT_INVALID.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
} else {
return buildBadRequestResponse(exception, uriInfo);
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
index ff6a5fe..47e8e64 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
@@ -397,7 +397,8 @@ public class AccountResource extends JaxRsResourceBase {
final List<RefundJson> result = new ArrayList<RefundJson>(Collections2.transform(refunds, new Function<Refund, RefundJson>() {
@Override
public RefundJson apply(Refund input) {
- return new RefundJson(input);
+ // TODO Return adjusted items and audits
+ return new RefundJson(input, null, null);
}
}));
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/CreditResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/CreditResource.java
index 25a1775..6ce38af 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/CreditResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/CreditResource.java
@@ -85,13 +85,13 @@ public class CreditResource extends JaxRsResourceBase {
@HeaderParam(HDR_CREATED_BY) final String createdBy,
@HeaderParam(HDR_REASON) final String reason,
@HeaderParam(HDR_COMMENT) final String comment) throws AccountApiException, InvoiceApiException {
- final Account account = accountUserApi.getAccountById(json.getAccountId());
+ final Account account = accountUserApi.getAccountById(UUID.fromString(json.getAccountId()));
final LocalDate effectiveDate = json.getEffectiveDate().toDateTime(account.getTimeZone()).toLocalDate();
final InvoiceItem credit;
if (json.getInvoiceId() != null) {
// Apply an invoice level credit
- credit = invoiceUserApi.insertCreditForInvoice(account.getId(), json.getInvoiceId(), json.getCreditAmount(),
+ credit = invoiceUserApi.insertCreditForInvoice(account.getId(), UUID.fromString(json.getInvoiceId()), json.getCreditAmount(),
effectiveDate, account.getCurrency(), context.createContext(createdBy, reason, comment));
} else {
// Apply a account level credit
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java
index 90be987..265a608 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java
@@ -53,9 +53,11 @@ import com.ning.billing.account.api.AccountApiException;
import com.ning.billing.account.api.AccountUserApi;
import com.ning.billing.invoice.api.Invoice;
import com.ning.billing.invoice.api.InvoiceApiException;
+import com.ning.billing.invoice.api.InvoiceItem;
import com.ning.billing.invoice.api.InvoiceNotifier;
import com.ning.billing.invoice.api.InvoiceUserApi;
import com.ning.billing.jaxrs.json.CustomFieldJson;
+import com.ning.billing.jaxrs.json.InvoiceItemJsonSimple;
import com.ning.billing.jaxrs.json.InvoiceJsonSimple;
import com.ning.billing.jaxrs.json.InvoiceJsonWithItems;
import com.ning.billing.jaxrs.json.PaymentJsonSimple;
@@ -69,6 +71,7 @@ import com.ning.billing.util.api.TagApiException;
import com.ning.billing.util.api.TagDefinitionApiException;
import com.ning.billing.util.api.TagUserApi;
import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.dao.ObjectType;
@Path(JaxrsResource.INVOICES_PATH)
@@ -87,6 +90,7 @@ public class InvoiceResource extends JaxRsResourceBase {
private final Context context;
private final JaxrsUriBuilder uriBuilder;
private final InvoiceNotifier invoiceNotifier;
+ private final Clock clock;
@Inject
public InvoiceResource(final AccountUserApi accountApi,
@@ -96,7 +100,8 @@ public class InvoiceResource extends JaxRsResourceBase {
final JaxrsUriBuilder uriBuilder,
final TagUserApi tagUserApi,
final CustomFieldUserApi customFieldUserApi,
- final InvoiceNotifier invoiceNotifier) {
+ final InvoiceNotifier invoiceNotifier,
+ final Clock clock) {
super(uriBuilder, tagUserApi, customFieldUserApi);
this.accountApi = accountApi;
this.invoiceApi = invoiceApi;
@@ -104,28 +109,39 @@ public class InvoiceResource extends JaxRsResourceBase {
this.context = context;
this.uriBuilder = uriBuilder;
this.invoiceNotifier = invoiceNotifier;
+ this.clock = clock;
}
@GET
@Produces(APPLICATION_JSON)
- public Response getInvoices(@QueryParam(QUERY_ACCOUNT_ID) final String accountId) throws AccountApiException {
+ public Response getInvoices(@QueryParam(QUERY_ACCOUNT_ID) final String accountId,
+ @QueryParam(QUERY_INVOICE_WITH_ITEMS) @DefaultValue("false") final boolean withItems) throws AccountApiException {
// Verify the account exists
accountApi.getAccountById(UUID.fromString(accountId));
final List<Invoice> invoices = invoiceApi.getInvoicesByAccount(UUID.fromString(accountId));
- final List<InvoiceJsonSimple> result = new LinkedList<InvoiceJsonSimple>();
- for (final Invoice invoice : invoices) {
- result.add(new InvoiceJsonSimple(invoice));
- }
+ if (withItems) {
+ final List<InvoiceJsonWithItems> result = new LinkedList<InvoiceJsonWithItems>();
+ for (final Invoice invoice : invoices) {
+ result.add(new InvoiceJsonWithItems(invoice));
+ }
- return Response.status(Status.OK).entity(result).build();
+ return Response.status(Status.OK).entity(result).build();
+ } else {
+ final List<InvoiceJsonSimple> result = new LinkedList<InvoiceJsonSimple>();
+ for (final Invoice invoice : invoices) {
+ result.add(new InvoiceJsonSimple(invoice));
+ }
+
+ return Response.status(Status.OK).entity(result).build();
+ }
}
@GET
@Path("/{invoiceId:" + UUID_PATTERN + "}/")
@Produces(APPLICATION_JSON)
public Response getInvoice(@PathParam("invoiceId") final String invoiceId,
- @QueryParam("withItems") @DefaultValue("false") final boolean withItems) throws InvoiceApiException {
+ @QueryParam(QUERY_INVOICE_WITH_ITEMS) @DefaultValue("false") final boolean withItems) throws InvoiceApiException {
final Invoice invoice = invoiceApi.getInvoice(UUID.fromString(invoiceId));
if (invoice == null) {
throw new InvoiceApiException(ErrorCode.INVOICE_NOT_FOUND);
@@ -165,6 +181,47 @@ public class InvoiceResource extends JaxRsResourceBase {
}
}
+ @POST
+ @Path("/{invoiceId:" + UUID_PATTERN + "}")
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response adjustInvoiceItem(final InvoiceItemJsonSimple json,
+ @PathParam("invoiceId") final String invoiceId,
+ @QueryParam(QUERY_REQUESTED_DT) final String requestedDateTimeString,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment) throws AccountApiException, InvoiceApiException {
+ final Account account = accountApi.getAccountById(UUID.fromString(json.getAccountId()));
+
+ // Get the effective date of the adjustment, in the account timezone
+ final LocalDate requestedDate;
+ if (requestedDateTimeString == null) {
+ requestedDate = clock.getUTCToday();
+ } else {
+ final DateTime requestedDateTime = DATE_TIME_FORMATTER.parseDateTime(requestedDateTimeString);
+ requestedDate = requestedDateTime.toDateTime(account.getTimeZone()).toLocalDate();
+ }
+
+ final InvoiceItem adjustmentItem;
+ if (json.getAmount() == null) {
+ adjustmentItem = invoiceApi.insertInvoiceItemAdjustment(account.getId(),
+ UUID.fromString(invoiceId),
+ UUID.fromString(json.getInvoiceItemId()),
+ requestedDate,
+ context.createContext(createdBy, reason, comment));
+ } else {
+ adjustmentItem = invoiceApi.insertInvoiceItemAdjustment(account.getId(),
+ UUID.fromString(invoiceId),
+ UUID.fromString(json.getInvoiceItemId()),
+ requestedDate,
+ json.getAmount(),
+ json.getCurrency(),
+ context.createContext(createdBy, reason, comment));
+ }
+
+ return uriBuilder.buildResponse(InvoiceResource.class, "getInvoice", adjustmentItem.getInvoiceId());
+ }
+
@GET
@Path("/{invoiceId:" + UUID_PATTERN + "}/" + PAYMENTS)
@Produces(APPLICATION_JSON)
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
index 037d036..567a72b 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
@@ -51,6 +51,8 @@ public interface JaxrsResource {
public static final String QUERY_ACCOUNT_ID = "accountId";
+ public static final String QUERY_INVOICE_WITH_ITEMS = "withItems";
+
public static final String QUERY_PAYMENT_EXTERNAL = "externalPayment";
public static final String QUERY_PAYMENT_LAST4_CC = "last4CC";
public static final String QUERY_PAYMENT_NAME_ON_CC = "nameOnCC";
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
index 8ee10fa..3f49504 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
@@ -16,8 +16,11 @@
package com.ning.billing.jaxrs.resources;
+import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import javax.ws.rs.Consumes;
@@ -37,6 +40,7 @@ import com.ning.billing.account.api.Account;
import com.ning.billing.account.api.AccountApiException;
import com.ning.billing.account.api.AccountUserApi;
import com.ning.billing.jaxrs.json.CustomFieldJson;
+import com.ning.billing.jaxrs.json.InvoiceItemJsonSimple;
import com.ning.billing.jaxrs.json.RefundJson;
import com.ning.billing.jaxrs.util.Context;
import com.ning.billing.jaxrs.util.JaxrsUriBuilder;
@@ -88,7 +92,8 @@ public class PaymentResource extends JaxRsResourceBase {
final List<RefundJson> result = new ArrayList<RefundJson>(Collections2.transform(refunds, new Function<Refund, RefundJson>() {
@Override
public RefundJson apply(final Refund input) {
- return new RefundJson(input);
+ // TODO Return adjusted items and audits
+ return new RefundJson(input, null, null);
}
}));
@@ -109,7 +114,22 @@ public class PaymentResource extends JaxRsResourceBase {
final Payment payment = paymentApi.getPayment(paymentUuid);
final Account account = accountApi.getAccountById(payment.getAccountId());
- final Refund result = paymentApi.createRefund(account, paymentUuid, json.getRefundAmount(), json.isAdjusted(), context.createContext(createdBy, reason, comment));
+ final Refund result;
+ if (json.isAdjusted()) {
+ if (json.getAdjustments() != null && json.getAdjustments().size() > 0) {
+ final Map<UUID, BigDecimal> adjustments = new HashMap<UUID, BigDecimal>();
+ for (final InvoiceItemJsonSimple item : json.getAdjustments()) {
+ adjustments.put(UUID.fromString(item.getInvoiceItemId()), item.getAmount());
+ }
+ result = paymentApi.createRefundWithItemsAdjustments(account, paymentUuid, adjustments, context.createContext(createdBy, reason, comment));
+ } else {
+ // Invoice adjustment
+ result = paymentApi.createRefundWithAdjustment(account, paymentUuid, json.getRefundAmount(), context.createContext(createdBy, reason, comment));
+ }
+ } else {
+ // Refund without adjustment
+ result = paymentApi.createRefund(account, paymentUuid, json.getRefundAmount(), context.createContext(createdBy, reason, comment));
+ }
return uriBuilder.buildResponse(RefundResource.class, "getRefund", result.getId(), uriInfo.getBaseUri().toString());
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/RefundResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/RefundResource.java
index fe1e3c7..c50c17a 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/RefundResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/RefundResource.java
@@ -57,7 +57,8 @@ public class RefundResource extends JaxRsResourceBase {
@Produces(APPLICATION_JSON)
public Response getRefund(@PathParam("refundId") final String refundId) throws PaymentApiException {
final Refund refund = paymentApi.getRefund(UUID.fromString(refundId));
- return Response.status(Status.OK).entity(new RefundJson(refund)).build();
+ // TODO Return adjusted items and audits
+ return Response.status(Status.OK).entity(new RefundJson(refund, null, null)).build();
}
@Override
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditCollectionJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditCollectionJson.java
index 04e8177..6e58ae5 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditCollectionJson.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditCollectionJson.java
@@ -36,10 +36,10 @@ public class TestCreditCollectionJson extends JaxrsTestSuite {
@Test(groups = "fast")
public void testJson() throws Exception {
- final UUID accountId = UUID.randomUUID();
+ final String accountId = UUID.randomUUID().toString();
final BigDecimal creditAmount = BigDecimal.TEN;
- final UUID invoiceId = UUID.randomUUID();
+ final String invoiceId = UUID.randomUUID().toString();
final String invoiceNumber = UUID.randomUUID().toString();
final DateTime requestedDate = clock.getUTCNow();
final DateTime effectiveDate = clock.getUTCNow();
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java
index 913c5ec..e63ba91 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java
@@ -35,12 +35,12 @@ public class TestCreditJson extends JaxrsTestSuite {
@Test(groups = "fast")
public void testJson() throws Exception {
final BigDecimal creditAmount = BigDecimal.TEN;
- final UUID invoiceId = UUID.randomUUID();
+ final String invoiceId = UUID.randomUUID().toString();
final String invoiceNumber = UUID.randomUUID().toString();
final DateTime requestedDate = clock.getUTCNow();
final DateTime effectiveDate = clock.getUTCNow();
final String reason = UUID.randomUUID().toString();
- final UUID accountId = UUID.randomUUID();
+ final String accountId = UUID.randomUUID().toString();
final List<AuditLogJson> auditLogs = createAuditLogsJson();
final CreditJson creditJson = new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate, effectiveDate,
reason, accountId, auditLogs);
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
index d312294..4b786ac 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
@@ -110,12 +110,12 @@ public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
private CreditJson createCreditJson() {
final BigDecimal creditAmount = BigDecimal.TEN;
- final UUID invoiceId = UUID.randomUUID();
+ final String invoiceId = UUID.randomUUID().toString();
final String invoiceNumber = UUID.randomUUID().toString();
final DateTime requestedDate = clock.getUTCNow();
final DateTime effectiveDate = clock.getUTCNow();
final String reason = UUID.randomUUID().toString();
- final UUID accountId = UUID.randomUUID();
+ final String accountId = UUID.randomUUID().toString();
return new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate, effectiveDate, reason, accountId, null);
}
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestRefundJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestRefundJson.java
new file mode 100644
index 0000000..d4dde15
--- /dev/null
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestRefundJson.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.jaxrs.JaxrsTestSuite;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestRefundJson extends JaxrsTestSuite {
+
+ private final Clock clock = new ClockMock();
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String refundId = UUID.randomUUID().toString();
+ final String paymentId = UUID.randomUUID().toString();
+ final BigDecimal refundAmount = BigDecimal.TEN;
+ final boolean isAdjusted = true;
+ final DateTime requestedDate = clock.getUTCNow();
+ final DateTime effectiveDate = clock.getUTCNow();
+ final List<InvoiceItemJsonSimple> adjustments = ImmutableList.<InvoiceItemJsonSimple>of(createInvoiceItemJson());
+ final List<AuditLogJson> auditLogs = createAuditLogsJson();
+ final RefundJson refundJson = new RefundJson(refundId, paymentId, refundAmount, isAdjusted, requestedDate,
+ effectiveDate, adjustments, auditLogs);
+ Assert.assertEquals(refundJson.getRefundId(), refundId);
+ Assert.assertEquals(refundJson.getPaymentId(), paymentId);
+ Assert.assertEquals(refundJson.getRefundAmount(), refundAmount);
+ Assert.assertEquals(refundJson.isAdjusted(), isAdjusted);
+ Assert.assertEquals(refundJson.getRequestedDate(), requestedDate);
+ Assert.assertEquals(refundJson.getEffectiveDate(), effectiveDate);
+ Assert.assertEquals(refundJson.getAdjustments(), adjustments);
+ Assert.assertEquals(refundJson.getAuditLogs(), auditLogs);
+
+ final String asJson = mapper.writeValueAsString(refundJson);
+ final RefundJson fromJson = mapper.readValue(asJson, RefundJson.class);
+ Assert.assertEquals(fromJson, refundJson);
+ }
+
+ private InvoiceItemJsonSimple createInvoiceItemJson() {
+ final String invoiceItemId = UUID.randomUUID().toString();
+ final String invoiceId = UUID.randomUUID().toString();
+ final String accountId = UUID.randomUUID().toString();
+ final String bundleId = UUID.randomUUID().toString();
+ final String subscriptionId = UUID.randomUUID().toString();
+ final String planName = UUID.randomUUID().toString();
+ final String phaseName = UUID.randomUUID().toString();
+ final String description = UUID.randomUUID().toString();
+ final LocalDate startDate = clock.getUTCToday();
+ final LocalDate endDate = clock.getUTCToday();
+ final BigDecimal amount = BigDecimal.TEN;
+ final Currency currency = Currency.MXN;
+ final List<AuditLogJson> auditLogs = createAuditLogsJson();
+ return new InvoiceItemJsonSimple(invoiceItemId, invoiceId, accountId, bundleId, subscriptionId,
+ planName, phaseName, description, startDate, endDate,
+ amount, currency, auditLogs);
+ }
+}
diff --git a/junction/src/main/java/com/ning/billing/junction/plumbing/billing/DefaultBillingApi.java b/junction/src/main/java/com/ning/billing/junction/plumbing/billing/DefaultBillingApi.java
index 9777935..19f0f28 100644
--- a/junction/src/main/java/com/ning/billing/junction/plumbing/billing/DefaultBillingApi.java
+++ b/junction/src/main/java/com/ning/billing/junction/plumbing/billing/DefaultBillingApi.java
@@ -113,9 +113,9 @@ public class DefaultBillingApi implements BillingApi {
private void debugLog(final SortedSet<BillingEvent> result, final String title) {
- log.debug(title);
+ log.info(title);
for (final BillingEvent aResult : result) {
- log.debug(aResult.toString());
+ log.info(aResult.toString());
}
}
diff --git a/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
index b4631a2..a2c1f11 100644
--- a/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
@@ -17,7 +17,9 @@
package com.ning.billing.payment.api;
import java.math.BigDecimal;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -28,6 +30,7 @@ import com.ning.billing.payment.core.PaymentProcessor;
import com.ning.billing.payment.core.RefundProcessor;
import com.ning.billing.util.callcontext.CallContext;
+import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
public class DefaultPaymentApi implements PaymentApi {
@@ -82,14 +85,34 @@ public class DefaultPaymentApi implements PaymentApi {
}
@Override
- public Refund createRefund(final Account account, final UUID paymentId,
- final BigDecimal refundAmount, final boolean isAdjusted, final CallContext context)
- throws PaymentApiException {
+ public Refund createRefund(final Account account, final UUID paymentId, final BigDecimal refundAmount, final CallContext context) throws PaymentApiException {
+ if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_REFUND_AMOUNT_NEGATIVE_OR_NULL);
+ }
+ return refundProcessor.createRefund(account, paymentId, refundAmount, false, ImmutableMap.<UUID, BigDecimal>of(), context);
+ }
+
+ @Override
+ public Refund createRefundWithAdjustment(final Account account, final UUID paymentId, final BigDecimal refundAmount, final CallContext context) throws PaymentApiException {
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new PaymentApiException(ErrorCode.PAYMENT_REFUND_AMOUNT_NEGATIVE_OR_NULL);
}
- return refundProcessor.createRefund(account, paymentId, refundAmount, isAdjusted, context);
+ return refundProcessor.createRefund(account, paymentId, refundAmount, true, ImmutableMap.<UUID, BigDecimal>of(), context);
+ }
+
+ @Override
+ public Refund createRefundWithItemsAdjustments(final Account account, final UUID paymentId, final Set<UUID> invoiceItemIds, final CallContext context) throws PaymentApiException {
+ final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts = new HashMap<UUID, BigDecimal>();
+ for (final UUID invoiceItemId : invoiceItemIds) {
+ invoiceItemIdsWithAmounts.put(invoiceItemId, null);
+ }
+ return refundProcessor.createRefund(account, paymentId, null, true, invoiceItemIdsWithAmounts, context);
+ }
+
+ @Override
+ public Refund createRefundWithItemsAdjustments(final Account account, final UUID paymentId, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final CallContext context) throws PaymentApiException {
+ return refundProcessor.createRefund(account, paymentId, null, true, invoiceItemIdsWithAmounts, context);
}
@Override
diff --git a/payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java b/payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java
index 69fbb4b..63706ba 100644
--- a/payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java
+++ b/payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java
@@ -13,36 +13,34 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-package com.ning.billing.payment.core;
-
-import javax.inject.Inject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+package com.ning.billing.payment.core;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Collections2;
-import com.google.inject.name.Named;
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import com.ning.billing.ErrorCode;
import com.ning.billing.account.api.Account;
import com.ning.billing.account.api.AccountApiException;
import com.ning.billing.account.api.AccountUserApi;
import com.ning.billing.invoice.api.InvoiceApiException;
+import com.ning.billing.invoice.api.InvoiceItem;
import com.ning.billing.invoice.api.InvoicePaymentApi;
import com.ning.billing.payment.api.DefaultRefund;
import com.ning.billing.payment.api.PaymentApiException;
-import com.ning.billing.payment.api.PaymentStatus;
import com.ning.billing.payment.api.Refund;
-import com.ning.billing.payment.dao.PaymentAttemptModelDao;
import com.ning.billing.payment.dao.PaymentDao;
import com.ning.billing.payment.dao.PaymentModelDao;
import com.ning.billing.payment.dao.RefundModelDao;
@@ -57,37 +55,59 @@ import com.ning.billing.util.callcontext.CallOrigin;
import com.ning.billing.util.callcontext.UserType;
import com.ning.billing.util.globallocker.GlobalLocker;
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.name.Named;
+
import static com.ning.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED;
public class RefundProcessor extends ProcessorBase {
- private final static Logger log = LoggerFactory.getLogger(RefundProcessor.class);
+ private static final Logger log = LoggerFactory.getLogger(RefundProcessor.class);
private final InvoicePaymentApi invoicePaymentApi;
private final CallContextFactory factory;
@Inject
public RefundProcessor(final PaymentProviderPluginRegistry pluginRegistry,
- final AccountUserApi accountUserApi,
- final InvoicePaymentApi invoicePaymentApi,
- final Bus eventBus,
- final CallContextFactory factory,
- final PaymentDao paymentDao,
- final GlobalLocker locker,
- @Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor) {
+ final AccountUserApi accountUserApi,
+ final InvoicePaymentApi invoicePaymentApi,
+ final Bus eventBus,
+ final CallContextFactory factory,
+ final PaymentDao paymentDao,
+ final GlobalLocker locker,
+ @Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor) {
super(pluginRegistry, accountUserApi, eventBus, paymentDao, locker, executor);
this.invoicePaymentApi = invoicePaymentApi;
this.factory = factory;
}
-
- public Refund createRefund(final Account account, final UUID paymentId, final BigDecimal refundAmount, final boolean isAdjusted, final CallContext context)
- throws PaymentApiException {
+ /**
+ * Create a refund and adjust the invoice or invoice items as necessary.
+ *
+ * @param account account to refund
+ * @param paymentId payment associated with that refund
+ * @param specifiedRefundAmount amount to refund. If null, the amount will be the sum of adjusted invoice items
+ * @param isAdjusted whether the refund should trigger an invoice or invoice item adjustment
+ * @param invoiceItemIdsWithAmounts invoice item ids and associated amounts to adjust
+ * @param context the call context
+ * @return the created context
+ * @throws PaymentApiException
+ */
+ public Refund createRefund(final Account account, final UUID paymentId, @Nullable final BigDecimal specifiedRefundAmount,
+ final boolean isAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final CallContext context)
+ throws PaymentApiException {
return new WithAccountLock<Refund>().processAccountWithLock(locker, account.getExternalKey(), new WithAccountLockCallback<Refund>() {
@Override
public Refund doOperation() throws PaymentApiException {
+ // First, compute the refund amount, if necessary
+ final BigDecimal refundAmount = computeRefundAmount(paymentId, specifiedRefundAmount, invoiceItemIdsWithAmounts);
+
try {
final PaymentModelDao payment = paymentDao.getPayment(paymentId);
@@ -104,8 +124,8 @@ public class RefundProcessor extends ProcessorBase {
int foundPluginCompletedRefunds = 0;
RefundModelDao refundInfo = null;
BigDecimal totalAmountRefunded = BigDecimal.ZERO;
- List<RefundModelDao> existingRefunds = paymentDao.getRefundsForPayment(paymentId);
- for (RefundModelDao cur : existingRefunds) {
+ final List<RefundModelDao> existingRefunds = paymentDao.getRefundsForPayment(paymentId);
+ for (final RefundModelDao cur : existingRefunds) {
final BigDecimal existingPositiveAmount = cur.getAmount();
if (existingPositiveAmount.compareTo(refundAmount) == 0) {
@@ -132,7 +152,7 @@ public class RefundProcessor extends ProcessorBase {
}
final PaymentPluginApi plugin = getPaymentProviderPlugin(payment.getPaymentMethodId());
- int nbExistingRefunds = plugin.getNbRefundForPaymentAmount(account, paymentId, refundAmount);
+ final int nbExistingRefunds = plugin.getNbRefundForPaymentAmount(account, paymentId, refundAmount);
log.debug(String.format("found %d pluginRefunds for paymentId %s and amount %s", nbExistingRefunds, paymentId, refundAmount));
if (nbExistingRefunds > foundPluginCompletedRefunds) {
@@ -143,7 +163,7 @@ public class RefundProcessor extends ProcessorBase {
}
paymentDao.updateRefundStatus(refundInfo.getId(), RefundStatus.PLUGIN_COMPLETED, context);
- invoicePaymentApi.createRefund(paymentId, refundAmount, isAdjusted, refundInfo.getId(), context);
+ invoicePaymentApi.createRefund(paymentId, refundAmount, isAdjusted, invoiceItemIdsWithAmounts, refundInfo.getId(), context);
paymentDao.updateRefundStatus(refundInfo.getId(), RefundStatus.COMPLETED, context);
@@ -158,14 +178,48 @@ public class RefundProcessor extends ProcessorBase {
});
}
+ /**
+ * Compute the refund amount (computed from the invoice or invoice items as necessary).
+ *
+ * @param paymentId payment id associated with this refund
+ * @param specifiedRefundAmount amount to refund. If null, the amount will be the sum of adjusted invoice items
+ * @param invoiceItemIdsWithAmounts invoice item ids and associated amounts to adjust
+ * @return the refund amount
+ */
+ private BigDecimal computeRefundAmount(final UUID paymentId, @Nullable final BigDecimal specifiedRefundAmount, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts) {
+ final List<InvoiceItem> items = invoicePaymentApi.getInvoiceForPaymentId(paymentId).getInvoiceItems();
+
+ BigDecimal amountFromItems = BigDecimal.ZERO;
+ for (final UUID itemId : invoiceItemIdsWithAmounts.keySet()) {
+ amountFromItems = amountFromItems.add(Objects.firstNonNull(invoiceItemIdsWithAmounts.get(itemId),
+ getAmountFromItem(items, itemId)));
+ }
+
+ // Sanity check: if some items were specified, then the sum should be equal to specified refund amount, if specified
+ if (amountFromItems.compareTo(BigDecimal.ZERO) != 0 && specifiedRefundAmount != null && specifiedRefundAmount.compareTo(amountFromItems) != 0) {
+ throw new IllegalArgumentException("You can't specify a refund amount that doesn't match the invoice items amounts");
+ }
+
+ return Objects.firstNonNull(specifiedRefundAmount, amountFromItems);
+ }
+
+ private BigDecimal getAmountFromItem(final List<InvoiceItem> items, final UUID itemId) {
+ for (final InvoiceItem item : items) {
+ if (item.getId().equals(itemId)) {
+ return item.getAmount();
+ }
+ }
+
+ throw new IllegalArgumentException("Unable to find invoice item for id " + itemId);
+ }
public Refund getRefund(final UUID refundId)
- throws PaymentApiException {
+ throws PaymentApiException {
RefundModelDao result = paymentDao.getRefund(refundId);
if (result == null) {
throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_REFUND, refundId);
}
- List<RefundModelDao> filteredInput = filterUncompletedPluginRefund(Collections.singletonList(result));
+ final List<RefundModelDao> filteredInput = filterUncompletedPluginRefund(Collections.singletonList(result));
if (filteredInput.size() == 0) {
throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_REFUND, refundId);
}
@@ -177,31 +231,30 @@ public class RefundProcessor extends ProcessorBase {
result.isAdjsuted(), result.getCreatedDate());
}
-
public List<Refund> getAccountRefunds(final Account account)
- throws PaymentApiException {
+ throws PaymentApiException {
List<RefundModelDao> result = paymentDao.getRefundsForAccount(account.getId());
if (completePluginCompletedRefund(result)) {
result = paymentDao.getRefundsForAccount(account.getId());
}
- List<RefundModelDao> filteredInput = filterUncompletedPluginRefund(result);
+ final List<RefundModelDao> filteredInput = filterUncompletedPluginRefund(result);
return toRefunds(filteredInput);
}
public List<Refund> getPaymentRefunds(final UUID paymentId)
- throws PaymentApiException {
+ throws PaymentApiException {
List<RefundModelDao> result = paymentDao.getRefundsForPayment(paymentId);
if (completePluginCompletedRefund(result)) {
result = paymentDao.getRefundsForPayment(paymentId);
}
- List<RefundModelDao> filteredInput = filterUncompletedPluginRefund(result);
+ final List<RefundModelDao> filteredInput = filterUncompletedPluginRefund(result);
return toRefunds(filteredInput);
}
public List<Refund> toRefunds(final List<RefundModelDao> in) {
return new ArrayList<Refund>(Collections2.transform(in, new Function<RefundModelDao, Refund>() {
@Override
- public Refund apply(RefundModelDao cur) {
+ public Refund apply(final RefundModelDao cur) {
return new DefaultRefund(cur.getId(), cur.getPaymentId(), cur.getAmount(), cur.getCurrency(),
cur.isAdjsuted(), cur.getCreatedDate());
}
@@ -211,7 +264,7 @@ public class RefundProcessor extends ProcessorBase {
private List<RefundModelDao> filterUncompletedPluginRefund(final List<RefundModelDao> input) {
return new ArrayList<RefundModelDao>(Collections2.filter(input, new Predicate<RefundModelDao>() {
@Override
- public boolean apply(RefundModelDao in) {
+ public boolean apply(final RefundModelDao in) {
return in.getRefundStatus() != RefundStatus.CREATED;
}
}));
@@ -219,10 +272,9 @@ public class RefundProcessor extends ProcessorBase {
private boolean completePluginCompletedRefund(final List<RefundModelDao> refunds) throws PaymentApiException {
-
final Collection<RefundModelDao> refundsToBeFixed = Collections2.filter(refunds, new Predicate<RefundModelDao>() {
@Override
- public boolean apply(RefundModelDao in) {
+ public boolean apply(final RefundModelDao in) {
return in.getRefundStatus() == RefundStatus.PLUGIN_COMPLETED;
}
});
@@ -231,15 +283,16 @@ public class RefundProcessor extends ProcessorBase {
}
try {
- Account account = accountUserApi.getAccountById(refundsToBeFixed.iterator().next().getAccountId());
+ final Account account = accountUserApi.getAccountById(refundsToBeFixed.iterator().next().getAccountId());
new WithAccountLock<Void>().processAccountWithLock(locker, account.getExternalKey(), new WithAccountLockCallback<Void>() {
@Override
public Void doOperation() throws PaymentApiException {
try {
final CallContext context = factory.createCallContext("RefundProcessor", CallOrigin.INTERNAL, UserType.SYSTEM);
- for (RefundModelDao cur : refundsToBeFixed) {
- invoicePaymentApi.createRefund(cur.getPaymentId(), cur.getAmount(), cur.isAdjsuted(), cur.getId(), context);
+ for (final RefundModelDao cur : refundsToBeFixed) {
+ // TODO - we currently don't save the items to be adjusted. If we crash, they won't be adjusted...
+ invoicePaymentApi.createRefund(cur.getPaymentId(), cur.getAmount(), cur.isAdjsuted(), ImmutableMap.<UUID, BigDecimal>of(), cur.getId(), context);
paymentDao.updateRefundStatus(cur.getId(), RefundStatus.COMPLETED, context);
}
} catch (InvoiceApiException e) {
@@ -253,15 +306,4 @@ public class RefundProcessor extends ProcessorBase {
throw new PaymentApiException(e);
}
}
-
- private PaymentAttemptModelDao getPaymentAttempt(final UUID paymentId) {
- List<PaymentAttemptModelDao> attempts = paymentDao.getAttemptsForPayment(paymentId);
- for (PaymentAttemptModelDao cur : attempts) {
- if (cur.getPaymentStatus() == PaymentStatus.SUCCESS) {
- return cur;
- }
- }
- return null;
- }
-
}
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestBundle.java b/server/src/test/java/com/ning/billing/jaxrs/TestBundle.java
index 94ae280..5a0945e 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestBundle.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestBundle.java
@@ -25,6 +25,7 @@ import java.util.Map;
import javax.ws.rs.core.Response.Status;
import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -125,7 +126,7 @@ public class TestBundle extends TestJaxrsBase {
final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), productName, ProductCategory.BASE.toString(), term.toString(), true);
Assert.assertNotNull(subscriptionJson.getChargedThroughDate());
- Assert.assertEquals(subscriptionJson.getChargedThroughDate().toString(), "2012-04-25T00:00:00.000Z");
+ Assert.assertEquals(subscriptionJson.getChargedThroughDate().toLocalDate(), new LocalDate("2012-04-25"));
final AccountJson newAccount = createAccountWithDefaultPaymentMethod("dst", "dst", "dst@yahoo.com");
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestCredit.java b/server/src/test/java/com/ning/billing/jaxrs/TestCredit.java
index d7b5dca..b459ce3 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestCredit.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestCredit.java
@@ -60,9 +60,9 @@ public class TestCredit extends TestJaxrsBase {
final DateTime requestedDate = clock.getUTCNow();
final DateTime effectiveDate = clock.getUTCNow();
final InvoiceJsonSimple invoice = createInvoice();
- final CreditJson input = new CreditJson(BigDecimal.TEN, UUID.fromString(invoice.getInvoiceId()), UUID.randomUUID().toString(),
+ final CreditJson input = new CreditJson(BigDecimal.TEN, invoice.getInvoiceId(), UUID.randomUUID().toString(),
requestedDate, effectiveDate,
- UUID.randomUUID().toString(), UUID.fromString(accountJson.getAccountId()),
+ UUID.randomUUID().toString(), accountJson.getAccountId(),
null);
final String jsonInput = mapper.writeValueAsString(input);
@@ -120,9 +120,9 @@ public class TestCredit extends TestJaxrsBase {
public void testAccountDoesNotExist() throws Exception {
final DateTime requestedDate = clock.getUTCNow();
final DateTime effectiveDate = clock.getUTCNow();
- final CreditJson input = new CreditJson(BigDecimal.TEN, UUID.randomUUID(), UUID.randomUUID().toString(),
+ final CreditJson input = new CreditJson(BigDecimal.TEN, UUID.randomUUID().toString(), UUID.randomUUID().toString(),
requestedDate, effectiveDate,
- UUID.randomUUID().toString(), UUID.randomUUID(), null);
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(), null);
final String jsonInput = mapper.writeValueAsString(input);
// Try to create the credit
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java b/server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java
index 4dfc5ab..675159a 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java
@@ -17,157 +17,69 @@
package com.ning.billing.jaxrs;
import java.math.BigDecimal;
-import java.util.HashMap;
+import java.math.RoundingMode;
import java.util.List;
-import java.util.Map;
-
-import javax.ws.rs.core.Response.Status;
import org.joda.time.DateTime;
-import org.joda.time.Interval;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.Assert;
import org.testng.annotations.Test;
-import com.ning.billing.catalog.api.BillingPeriod;
-import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.jaxrs.json.AccountJson;
-import com.ning.billing.jaxrs.json.BundleJsonNoSubscriptions;
+import com.ning.billing.jaxrs.json.InvoiceItemJsonSimple;
import com.ning.billing.jaxrs.json.InvoiceJsonSimple;
+import com.ning.billing.jaxrs.json.InvoiceJsonWithItems;
import com.ning.billing.jaxrs.json.PaymentJsonSimple;
import com.ning.billing.jaxrs.json.PaymentMethodJson;
-import com.ning.billing.jaxrs.json.SubscriptionJsonNoEvents;
-import com.ning.billing.jaxrs.resources.JaxrsResource;
import com.ning.billing.payment.provider.ExternalPaymentProviderPlugin;
-import com.ning.http.client.Response;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.google.common.collect.ImmutableMap;
import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
-import static org.testng.Assert.assertTrue;
public class TestInvoice extends TestJaxrsBase {
- private static final Logger log = LoggerFactory.getLogger(TestInvoice.class);
-
@Test(groups = "slow")
public void testInvoiceOk() throws Exception {
final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
- final AccountJson accountJson = createAccountWithDefaultPaymentMethod("poupou", "qhddffrwe", "poupou@yahoo.com");
- assertNotNull(accountJson);
-
- final BundleJsonNoSubscriptions bundleJson = createBundle(accountJson.getAccountId(), "9967599");
- assertNotNull(bundleJson);
-
- final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), "Shotgun", ProductCategory.BASE.toString(), BillingPeriod.MONTHLY.toString(), true);
- assertNotNull(subscriptionJson);
+ final AccountJson accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
- // MOVE AFTER TRIAL
- final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(30));
- clock.addDeltaFromReality(it.toDurationMillis());
- crappyWaitForLackOfProperSynchonization();
-
- String uri = JaxrsResource.INVOICES_PATH;
- final Map<String, String> queryParams = new HashMap<String, String>();
- queryParams.put(JaxrsResource.QUERY_ACCOUNT_ID, accountJson.getAccountId());
-
- Response response = doGet(uri, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
- Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- String baseJson = response.getResponseBody();
- List<InvoiceJsonSimple> objFromJson = mapper.readValue(baseJson, new TypeReference<List<InvoiceJsonSimple>>() {});
- assertNotNull(objFromJson);
- log.info(baseJson);
- assertEquals(objFromJson.size(), 2);
+ final List<InvoiceJsonSimple> invoices = getInvoicesForAccount(accountJson.getAccountId());
+ assertEquals(invoices.size(), 2);
// Check we can retrieve an individual invoice
- uri = JaxrsResource.INVOICES_PATH + "/" + objFromJson.get(0).getInvoiceId();
- response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
- Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- baseJson = response.getResponseBody();
- final InvoiceJsonSimple firstInvoiceJson = mapper.readValue(baseJson, InvoiceJsonSimple.class);
- assertNotNull(objFromJson);
- assertEquals(firstInvoiceJson, objFromJson.get(0));
+ final InvoiceJsonSimple invoiceJsonSimple = invoices.get(0);
+ final InvoiceJsonSimple firstInvoiceJson = getInvoice(invoiceJsonSimple.getInvoiceId());
+ assertEquals(firstInvoiceJson, invoiceJsonSimple);
// Then create a dryRun Invoice
final DateTime futureDate = clock.getUTCNow().plusMonths(1).plusDays(3);
- uri = JaxrsResource.INVOICES_PATH;
- queryParams.put(JaxrsResource.QUERY_TARGET_DATE, futureDate.toString());
- queryParams.put(JaxrsResource.QUERY_DRY_RUN, "true");
- response = doPost(uri, null, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
- Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- baseJson = response.getResponseBody();
- final InvoiceJsonSimple futureInvoice = mapper.readValue(baseJson, InvoiceJsonSimple.class);
- assertNotNull(futureInvoice);
- log.info(baseJson);
+ createDryRunInvoice(accountJson.getAccountId(), futureDate);
// The one more time with no DryRun
- queryParams.remove(JaxrsResource.QUERY_DRY_RUN);
- response = doPost(uri, null, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
- Assert.assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
-
- final String location = response.getHeader("Location");
- Assert.assertNotNull(location);
-
- // Check again # invoices, should be 5 this time
- uri = JaxrsResource.INVOICES_PATH;
- response = doGet(uri, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
- assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- baseJson = response.getResponseBody();
- objFromJson = mapper.readValue(baseJson, new TypeReference<List<InvoiceJsonSimple>>() {});
- assertNotNull(objFromJson);
- log.info(baseJson);
- assertEquals(objFromJson.size(), 3);
+ createInvoice(accountJson.getAccountId(), futureDate);
+
+ // Check again # invoices, should be 3 this time
+ final List<InvoiceJsonSimple> newInvoiceList = getInvoicesForAccount(accountJson.getAccountId());
+ assertEquals(newInvoiceList.size(), 3);
}
@Test(groups = "slow")
public void testInvoicePayments() throws Exception {
clock.setTime(new DateTime(2012, 4, 25, 0, 3, 42, 0));
- final AccountJson accountJson = createAccountWithDefaultPaymentMethod("nohup", "shtergyhwF", "nohup@yahoo.com");
- assertNotNull(accountJson);
-
- final BundleJsonNoSubscriptions bundleJson = createBundle(accountJson.getAccountId(), "391193");
- assertNotNull(bundleJson);
+ final AccountJson accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
- final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), "Shotgun", ProductCategory.BASE.toString(), BillingPeriod.MONTHLY.toString(), true);
- assertNotNull(subscriptionJson);
-
- clock.addDays(31);
-
- crappyWaitForLackOfProperSynchonization();
-
- final Map<String, String> queryParams = new HashMap<String, String>();
- queryParams.put(JaxrsResource.QUERY_ACCOUNT_ID, accountJson.getAccountId());
- String uri = JaxrsResource.INVOICES_PATH;
- Response response = doGet(uri, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
- assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- String baseJson = response.getResponseBody();
- final List<InvoiceJsonSimple> invoices = mapper.readValue(baseJson, new TypeReference<List<InvoiceJsonSimple>>() {});
- assertNotNull(invoices);
- log.info(baseJson);
+ final List<InvoiceJsonSimple> invoices = getInvoicesForAccount(accountJson.getAccountId());
assertEquals(invoices.size(), 2);
for (final InvoiceJsonSimple cur : invoices) {
+ final List<PaymentJsonSimple> objFromJson = getPaymentsForInvoice(cur.getInvoiceId());
- uri = JaxrsResource.INVOICES_PATH + "/" + cur.getInvoiceId() + "/" + JaxrsResource.PAYMENTS;
- response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
-
- Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- baseJson = response.getResponseBody();
- final List<PaymentJsonSimple> objFromJson = mapper.readValue(baseJson, new TypeReference<List<PaymentJsonSimple>>() {});
- assertNotNull(objFromJson);
- log.info(cur.getAmount().toString());
if (cur.getAmount().compareTo(BigDecimal.ZERO) == 0) {
assertEquals(objFromJson.size(), 0);
} else {
assertEquals(objFromJson.size(), 1);
- assertTrue(cur.getAmount().compareTo(objFromJson.get(0).getAmount()) == 0);
+ assertEquals(cur.getAmount().compareTo(objFromJson.get(0).getAmount()), 0);
}
}
}
@@ -176,31 +88,11 @@ public class TestInvoice extends TestJaxrsBase {
public void testInvoiceCreatePayment() throws Exception {
clock.setTime(new DateTime(2012, 4, 25, 0, 3, 42, 0));
- final AccountJson accountJson = createAccountWithDefaultPaymentMethod("nohup", "shtergyhwF", "nohup@yahoo.com");
- assertNotNull(accountJson);
-
// STEPH MISSING SET ACCOUNT AUTO_PAY_OFF
+ final AccountJson accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
- final BundleJsonNoSubscriptions bundleJson = createBundle(accountJson.getAccountId(), "391193");
- assertNotNull(bundleJson);
-
- final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), "Shotgun", ProductCategory.BASE.toString(), BillingPeriod.MONTHLY.toString(), true);
- assertNotNull(subscriptionJson);
-
- // MOVE AFTER TRIAL
- clock.addDays(31);
-
- crappyWaitForLackOfProperSynchonization();
-
- final Map<String, String> queryParams = new HashMap<String, String>();
- queryParams.put(JaxrsResource.QUERY_ACCOUNT_ID, accountJson.getAccountId());
- String uri = JaxrsResource.INVOICES_PATH;
- Response response = doGet(uri, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
- assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- String baseJson = response.getResponseBody();
- final List<InvoiceJsonSimple> invoices = mapper.readValue(baseJson, new TypeReference<List<InvoiceJsonSimple>>() {});
- assertNotNull(invoices);
- log.info(baseJson);
+ // Get the invoices
+ final List<InvoiceJsonSimple> invoices = getInvoicesForAccount(accountJson.getAccountId());
assertEquals(invoices.size(), 2);
for (final InvoiceJsonSimple cur : invoices) {
@@ -209,89 +101,89 @@ public class TestInvoice extends TestJaxrsBase {
}
// CREATE INSTA PAYMENT
- final PaymentJsonSimple payment = new PaymentJsonSimple(cur.getAmount(), BigDecimal.ZERO, accountJson.getAccountId(),
- cur.getInvoiceId(), null, null, null, null, 0, null, null, null, null, null, null, null);
- final String postJson = mapper.writeValueAsString(payment);
-
- uri = JaxrsResource.INVOICES_PATH + "/" + cur.getInvoiceId() + "/" + JaxrsResource.PAYMENTS;
- doPost(uri, postJson, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
-
- response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
-
- Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- baseJson = response.getResponseBody();
- final List<PaymentJsonSimple> objFromJson = mapper.readValue(baseJson, new TypeReference<List<PaymentJsonSimple>>() {});
- assertNotNull(objFromJson);
- log.info(cur.getAmount().toString());
+ final List<PaymentJsonSimple> objFromJson = createInstaPayment(accountJson, cur);
assertEquals(objFromJson.size(), 1);
- assertTrue(cur.getAmount().compareTo(objFromJson.get(0).getAmount()) == 0);
+ assertEquals(cur.getAmount().compareTo(objFromJson.get(0).getAmount()), 0);
}
}
@Test(groups = "slow")
public void testExternalPayment() throws Exception {
- // Create an account with no payment method
- final AccountJson accountJson = createAccount("eraahahildo", "sheqrgfhwe", "eraahahildo@yahoo.com");
- assertNotNull(accountJson);
-
- // Add a bundle, subscription and move the clock to get the first invoice
- final BundleJsonNoSubscriptions bundleJson = createBundle(accountJson.getAccountId(), "317199");
- assertNotNull(bundleJson);
- final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), "Shotgun", ProductCategory.BASE.toString(), BillingPeriod.MONTHLY.toString(), true);
- assertNotNull(subscriptionJson);
- clock.addMonths(1);
- crappyWaitForLackOfProperSynchonization();
-
- final String paymentsURI = JaxrsResource.ACCOUNTS_PATH + "/" + accountJson.getAccountId() + "/" + JaxrsResource.PAYMENTS;
+ final AccountJson accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
// Verify we didn't get any payment
- final Response noPaymentsResponse = doGet(paymentsURI, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
- assertEquals(noPaymentsResponse.getStatusCode(), Status.OK.getStatusCode());
- final String noPaymentsBaseJson = noPaymentsResponse.getResponseBody();
- final List<PaymentJsonSimple> noPaymentsFromJson = mapper.readValue(noPaymentsBaseJson, new TypeReference<List<PaymentJsonSimple>>() {});
+ final List<PaymentJsonSimple> noPaymentsFromJson = getPaymentsForAccount(accountJson.getAccountId());
assertEquals(noPaymentsFromJson.size(), 0);
// Get the invoices
- final Map<String, String> queryParams = new HashMap<String, String>();
- queryParams.put(JaxrsResource.QUERY_ACCOUNT_ID, accountJson.getAccountId());
- final String invoicesURI = JaxrsResource.INVOICES_PATH;
- final Response invoicesResponse = doGet(invoicesURI, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
- assertEquals(invoicesResponse.getStatusCode(), Status.OK.getStatusCode());
- final String invoicesBaseJson = invoicesResponse.getResponseBody();
- final List<InvoiceJsonSimple> invoices = mapper.readValue(invoicesBaseJson, new TypeReference<List<InvoiceJsonSimple>>() {});
- assertNotNull(invoices);
+ final List<InvoiceJsonSimple> invoices = getInvoicesForAccount(accountJson.getAccountId());
// 2 invoices but look for the non zero dollar one
assertEquals(invoices.size(), 2);
final String invoiceId = invoices.get(1).getInvoiceId();
// Post an external payment
final BigDecimal paidAmount = BigDecimal.TEN;
- final PaymentJsonSimple payment = new PaymentJsonSimple(paidAmount, BigDecimal.ZERO, accountJson.getAccountId(),
- invoiceId, null, null, null, null, 0,
- null, null, null, null, null, null, null);
- final String postJson = mapper.writeValueAsString(payment);
- final String paymentURI = JaxrsResource.INVOICES_PATH + "/" + invoiceId + "/" + JaxrsResource.PAYMENTS;
- final Response paymentResponse = doPost(paymentURI, postJson, ImmutableMap.<String, String>of("externalPayment", "true"), DEFAULT_HTTP_TIMEOUT_SEC);
- assertEquals(paymentResponse.getStatusCode(), Status.CREATED.getStatusCode());
+ createExternalPayment(accountJson, invoiceId, paidAmount);
// Verify we indeed got the payment
- final Response paymentsResponse = doGet(paymentsURI, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
- assertEquals(paymentsResponse.getStatusCode(), Status.OK.getStatusCode());
- final String paymentsBaseJson = paymentsResponse.getResponseBody();
- final List<PaymentJsonSimple> paymentsFromJson = mapper.readValue(paymentsBaseJson, new TypeReference<List<PaymentJsonSimple>>() {});
+ final List<PaymentJsonSimple> paymentsFromJson = getPaymentsForAccount(accountJson.getAccountId());
assertEquals(paymentsFromJson.size(), 1);
assertEquals(paymentsFromJson.get(0).getPaidAmount().compareTo(paidAmount), 0);
// Check the PaymentMethod from paymentMethodId returned in the Payment object
final String paymentMethodId = paymentsFromJson.get(0).getPaymentMethodId();
- final String paymentMethodURI = JaxrsResource.PAYMENT_METHODS_PATH + "/" + paymentMethodId;
-
- final Response paymentMethodResponse = doGet(paymentMethodURI, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
- assertEquals(paymentMethodResponse.getStatusCode(), Status.OK.getStatusCode());
- final PaymentMethodJson paymentMethodJson = mapper.readValue(paymentMethodResponse.getResponseBody(), PaymentMethodJson.class);
+ final PaymentMethodJson paymentMethodJson = getPaymentMethod(paymentMethodId);
assertEquals(paymentMethodJson.getPaymentMethodId(), paymentMethodId);
assertEquals(paymentMethodJson.getAccountId(), accountJson.getAccountId());
assertEquals(paymentMethodJson.getPluginName(), ExternalPaymentProviderPlugin.PLUGIN_NAME);
assertNull(paymentMethodJson.getPluginInfo());
}
+
+ @Test(groups = "slow")
+ public void testFullInvoiceItemAdjustment() throws Exception {
+ final AccountJson accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Get the invoices
+ final List<InvoiceJsonWithItems> invoices = getInvoicesWithItemsForAccount(accountJson.getAccountId());
+ // 2 invoices but look for the non zero dollar one
+ assertEquals(invoices.size(), 2);
+ final InvoiceJsonWithItems invoice = invoices.get(1);
+ // Verify the invoice we picked is non zero
+ assertEquals(invoice.getAmount().compareTo(BigDecimal.ZERO), 1);
+ final InvoiceItemJsonSimple invoiceItem = invoice.getItems().get(0);
+ // Verify the item we picked is non zero
+ assertEquals(invoiceItem.getAmount().compareTo(BigDecimal.ZERO), 1);
+
+ // Adjust the full amount
+ adjustInvoiceItem(accountJson.getAccountId(), invoice.getInvoiceId(), invoiceItem.getInvoiceItemId(), null, null, null);
+
+ // Verify the new invoice balance is zero
+ final InvoiceJsonSimple adjustedInvoice = getInvoice(invoice.getInvoiceId());
+ assertEquals(adjustedInvoice.getAmount().compareTo(BigDecimal.ZERO), 1);
+ }
+
+ @Test(groups = "slow")
+ public void testPartialInvoiceItemAdjustment() throws Exception {
+ final AccountJson accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Get the invoices
+ final List<InvoiceJsonWithItems> invoices = getInvoicesWithItemsForAccount(accountJson.getAccountId());
+ // 2 invoices but look for the non zero dollar one
+ assertEquals(invoices.size(), 2);
+ final InvoiceJsonWithItems invoice = invoices.get(1);
+ // Verify the invoice we picked is non zero
+ assertEquals(invoice.getAmount().compareTo(BigDecimal.ZERO), 1);
+ final InvoiceItemJsonSimple invoiceItem = invoice.getItems().get(0);
+ // Verify the item we picked is non zero
+ assertEquals(invoiceItem.getAmount().compareTo(BigDecimal.ZERO), 1);
+
+ // Adjust partially the item
+ final BigDecimal adjustedAmount = invoiceItem.getAmount().divide(BigDecimal.TEN);
+ adjustInvoiceItem(accountJson.getAccountId(), invoice.getInvoiceId(), invoiceItem.getInvoiceItemId(), null, adjustedAmount, null);
+
+ // Verify the new invoice balance
+ final InvoiceJsonSimple adjustedInvoice = getInvoice(invoice.getInvoiceId());
+ final BigDecimal adjustedInvoiceBalance = invoice.getBalance().add(adjustedAmount.negate().setScale(2, RoundingMode.HALF_UP));
+ assertEquals(adjustedInvoice.getBalance().compareTo(adjustedInvoiceBalance), 0);
+ }
}
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
index aba8426..c5334d9 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
@@ -13,12 +13,12 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
+
package com.ning.billing.jaxrs;
-import javax.annotation.Nullable;
-import javax.ws.rs.core.Response.Status;
import java.io.IOException;
import java.lang.reflect.Method;
+import java.math.BigDecimal;
import java.net.URL;
import java.util.ArrayList;
import java.util.EventListener;
@@ -30,30 +30,30 @@ import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+import javax.ws.rs.core.Response.Status;
+
import org.eclipse.jetty.servlet.FilterHolder;
+import org.joda.time.DateTime;
import org.skife.config.ConfigurationObjectFactory;
import org.skife.jdbi.v2.IDBI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
-import org.testng.annotations.AfterMethod;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.BeforeSuite;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
-import com.google.inject.Module;
import com.ning.billing.KillbillTestSuiteWithEmbeddedDB;
-import com.ning.billing.account.api.BillCycleDay;
import com.ning.billing.account.glue.AccountModule;
import com.ning.billing.analytics.setup.AnalyticsModule;
import com.ning.billing.api.TestApiListener;
import com.ning.billing.beatrix.glue.BeatrixModule;
-import com.ning.billing.beatrix.integration.TestIntegration;
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.Currency;
import com.ning.billing.catalog.api.PriceListSet;
+import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.catalog.glue.CatalogModule;
import com.ning.billing.config.PaymentConfig;
import com.ning.billing.dbi.DBIProvider;
@@ -66,9 +66,14 @@ import com.ning.billing.invoice.notification.NullInvoiceNotifier;
import com.ning.billing.jaxrs.json.AccountJson;
import com.ning.billing.jaxrs.json.BillCycleDayJson;
import com.ning.billing.jaxrs.json.BundleJsonNoSubscriptions;
+import com.ning.billing.jaxrs.json.InvoiceItemJsonSimple;
+import com.ning.billing.jaxrs.json.InvoiceJsonSimple;
+import com.ning.billing.jaxrs.json.InvoiceJsonWithItems;
+import com.ning.billing.jaxrs.json.PaymentJsonSimple;
import com.ning.billing.jaxrs.json.PaymentMethodJson;
import com.ning.billing.jaxrs.json.PaymentMethodJson.PaymentMethodPluginDetailJson;
import com.ning.billing.jaxrs.json.PaymentMethodJson.PaymentMethodProperties;
+import com.ning.billing.jaxrs.json.RefundJson;
import com.ning.billing.jaxrs.json.SubscriptionJsonNoEvents;
import com.ning.billing.jaxrs.resources.JaxrsResource;
import com.ning.billing.junction.glue.DefaultJunctionModule;
@@ -88,7 +93,6 @@ import com.ning.billing.util.glue.CustomFieldModule;
import com.ning.billing.util.glue.GlobalLockerModule;
import com.ning.billing.util.glue.NotificationQueueModule;
import com.ning.billing.util.glue.TagStoreModule;
-import com.ning.billing.util.io.IOUtils;
import com.ning.http.client.AsyncCompletionHandler;
import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.AsyncHttpClient.BoundRequestBuilder;
@@ -98,9 +102,19 @@ import com.ning.http.client.Response;
import com.ning.jetty.core.CoreConfig;
import com.ning.jetty.core.server.HttpServer;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Module;
+
+import static com.ning.billing.jaxrs.resources.JaxrsResource.QUERY_PAYMENT_METHOD_PLUGIN_INFO;
+import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
+
protected static final String PLUGIN_NAME = "noop";
// STEPH
@@ -140,6 +154,7 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
}
public static class TestKillbillGuiceListener extends KillbillGuiceListener {
+
private final MysqlTestingHelper helper;
private final Clock clock;
@@ -169,6 +184,7 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
}
public static class InvoiceModuleWithMockSender extends DefaultInvoiceModule {
+
@Override
protected void installInvoiceNotifier() {
bind(InvoiceNotifier.class).to(NullInvoiceNotifier.class).asEagerSingleton();
@@ -176,6 +192,7 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
}
public static class TestKillbillServerModule extends KillbillServerModule {
+
private final MysqlTestingHelper helper;
private final Clock clock;
@@ -191,6 +208,7 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
}
private static final class PaymentMockModule extends PaymentModule {
+
@Override
protected void installPaymentProviderPlugins(final PaymentConfig config) {
install(new MockPaymentProviderPluginModule(PLUGIN_NAME));
@@ -311,7 +329,6 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
return properties;
}
-
protected List<PaymentMethodProperties> getPaymentMethodPaypalProperties() {
final List<PaymentMethodProperties> properties = new ArrayList<PaymentMethodJson.PaymentMethodProperties>();
properties.add(new PaymentMethodProperties("type", "CreditCard", false));
@@ -320,12 +337,15 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
return properties;
}
-
protected PaymentMethodJson getPaymentMethodJson(final String accountId, final List<PaymentMethodProperties> properties) {
final PaymentMethodPluginDetailJson info = new PaymentMethodPluginDetailJson(null, properties);
return new PaymentMethodJson(null, accountId, true, PLUGIN_NAME, info);
}
+ //
+ // ACCOUNT UTILITIES
+ //
+
protected AccountJson createAccountWithDefaultPaymentMethod(final String name, final String key, final String email) throws Exception {
final AccountJson input = createAccount(name, key, email);
@@ -339,7 +359,6 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
Response response = doPost(uri, baseJson, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
Assert.assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
-
queryParams = new HashMap<String, String>();
queryParams.put(JaxrsResource.QUERY_EXTERNAL_KEY, input.getExternalKey());
response = doGet(JaxrsResource.ACCOUNTS_PATH, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
@@ -370,7 +389,6 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
return objFromJson;
}
-
protected BundleJsonNoSubscriptions createBundle(final String accountId, final String key) throws Exception {
final BundleJsonNoSubscriptions input = new BundleJsonNoSubscriptions(null, accountId, key, null, null);
String baseJson = mapper.writeValueAsString(input);
@@ -395,7 +413,6 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
final SubscriptionJsonNoEvents input = new SubscriptionJsonNoEvents(null, bundleId, null, productName, productCategory, billingPeriod, PriceListSet.DEFAULT_PRICELIST_NAME, null, null);
String baseJson = mapper.writeValueAsString(input);
-
final Map<String, String> queryParams = waitCompletion ? getQueryParamsForCallCompletion("5") : DEFAULT_EMPTY_QUERY;
Response response = doPost(JaxrsResource.SUBSCRIPTIONS_PATH, baseJson, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
Assert.assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
@@ -414,6 +431,273 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
return objFromJson;
}
+ //
+ // INVOICE UTILITIES
+ //
+
+ protected AccountJson createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice() throws Exception {
+ final AccountJson accountJson = createAccountWithDefaultPaymentMethod("nohup", "shtergyhwF", "nohup@yahoo.com");
+ assertNotNull(accountJson);
+
+ // Add a bundle, subscription and move the clock to get the first invoice
+ final BundleJsonNoSubscriptions bundleJson = createBundle(accountJson.getAccountId(), "391193");
+ assertNotNull(bundleJson);
+ final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), "Shotgun", ProductCategory.BASE.toString(), BillingPeriod.MONTHLY.toString(), true);
+ assertNotNull(subscriptionJson);
+ clock.addMonths(1);
+ crappyWaitForLackOfProperSynchonization();
+
+ return accountJson;
+ }
+
+ protected AccountJson createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice() throws Exception {
+ // Create an account with no payment method
+ final AccountJson accountJson = createAccount("eraahahildo", "sheqrgfhwe", "eraahahildo@yahoo.com");
+ assertNotNull(accountJson);
+
+ // Add a bundle, subscription and move the clock to get the first invoice
+ final BundleJsonNoSubscriptions bundleJson = createBundle(accountJson.getAccountId(), "317199");
+ assertNotNull(bundleJson);
+ final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), "Shotgun", ProductCategory.BASE.toString(), BillingPeriod.MONTHLY.toString(), true);
+ assertNotNull(subscriptionJson);
+ clock.addMonths(1);
+ crappyWaitForLackOfProperSynchonization();
+
+ // No payment will be triggered as the account doesn't have a payment method
+
+ return accountJson;
+ }
+
+ protected InvoiceJsonSimple getInvoice(final String invoiceId) throws IOException {
+ return doGetInvoice(invoiceId, Boolean.FALSE, InvoiceJsonSimple.class);
+ }
+
+ protected InvoiceJsonWithItems getInvoiceWithItems(final String invoiceId) throws IOException {
+ return doGetInvoice(invoiceId, Boolean.TRUE, InvoiceJsonWithItems.class);
+ }
+
+ private <T> T doGetInvoice(final String invoiceId, final Boolean withItems, final Class<T> clazz) throws IOException {
+ final String uri = JaxrsResource.INVOICES_PATH + "/" + invoiceId;
+
+ final Map<String, String> queryParams = new HashMap<String, String>();
+ queryParams.put(JaxrsResource.QUERY_INVOICE_WITH_ITEMS, withItems.toString());
+
+ final Response response = doGet(uri, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
+ Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
+ final String baseJson = response.getResponseBody();
+
+ final T firstInvoiceJson = mapper.readValue(baseJson, clazz);
+ assertNotNull(firstInvoiceJson);
+
+ return firstInvoiceJson;
+ }
+
+ protected List<InvoiceJsonSimple> getInvoicesForAccount(final String accountId) throws IOException {
+ return doGetInvoicesForAccount(accountId, Boolean.FALSE, InvoiceJsonSimple.class);
+ }
+
+ protected List<InvoiceJsonWithItems> getInvoicesWithItemsForAccount(final String accountId) throws IOException {
+ return doGetInvoicesForAccount(accountId, Boolean.TRUE, InvoiceJsonWithItems.class);
+ }
+
+ private <T> List<T> doGetInvoicesForAccount(final String accountId, final Boolean withItems, final Class<T> clazz) throws IOException {
+ final String invoicesURI = JaxrsResource.INVOICES_PATH;
+
+ final Map<String, String> queryParams = new HashMap<String, String>();
+ queryParams.put(JaxrsResource.QUERY_ACCOUNT_ID, accountId);
+ queryParams.put(JaxrsResource.QUERY_INVOICE_WITH_ITEMS, withItems.toString());
+
+ final Response invoicesResponse = doGet(invoicesURI, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
+ assertEquals(invoicesResponse.getStatusCode(), Status.OK.getStatusCode());
+
+ final String invoicesBaseJson = invoicesResponse.getResponseBody();
+ final List<T> invoices = mapper.readValue(invoicesBaseJson, new TypeReference<List<T>>() {});
+ assertNotNull(invoices);
+
+ return invoices;
+ }
+
+ protected InvoiceJsonSimple createDryRunInvoice(final String accountId, final DateTime futureDate) throws IOException {
+ final String uri = JaxrsResource.INVOICES_PATH;
+
+ final Map<String, String> queryParams = new HashMap<String, String>();
+ queryParams.put(JaxrsResource.QUERY_ACCOUNT_ID, accountId);
+ queryParams.put(JaxrsResource.QUERY_TARGET_DATE, futureDate.toString());
+ queryParams.put(JaxrsResource.QUERY_DRY_RUN, "true");
+
+ final Response response = doPost(uri, null, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
+ Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
+
+ final String baseJson = response.getResponseBody();
+ final InvoiceJsonSimple futureInvoice = mapper.readValue(baseJson, InvoiceJsonSimple.class);
+ assertNotNull(futureInvoice);
+
+ return futureInvoice;
+ }
+
+ protected void createInvoice(final String accountId, final DateTime futureDate) throws IOException {
+ final String uri = JaxrsResource.INVOICES_PATH;
+
+ final Map<String, String> queryParams = new HashMap<String, String>();
+ queryParams.put(JaxrsResource.QUERY_ACCOUNT_ID, accountId);
+ queryParams.put(JaxrsResource.QUERY_TARGET_DATE, futureDate.toString());
+
+ final Response response = doPost(uri, null, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
+ Assert.assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
+
+ final String location = response.getHeader("Location");
+ Assert.assertNotNull(location);
+ }
+
+ protected void adjustInvoiceItem(final String accountId, final String invoiceId, final String invoiceItemId,
+ @Nullable final DateTime requestedDate, @Nullable final BigDecimal amount, @Nullable final Currency currency) throws IOException {
+ final String uri = JaxrsResource.INVOICES_PATH + "/" + invoiceId;
+
+ final Map<String, String> queryParams = new HashMap<String, String>();
+ if (requestedDate != null) {
+ queryParams.put(JaxrsResource.QUERY_REQUESTED_DT, requestedDate.toDateTimeISO().toString());
+ }
+
+ final InvoiceItemJsonSimple adjustment = new InvoiceItemJsonSimple(invoiceItemId, null, accountId, null, null, null, null,
+ null, null, null, amount, currency, null);
+ final String adjustmentJson = mapper.writeValueAsString(adjustment);
+ final Response response = doPost(uri, adjustmentJson, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
+ Assert.assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
+ }
+
+ //
+ // PAYMENT UTILITIES
+ //
+
+ protected PaymentMethodJson getPaymentMethod(final String paymentMethodId) throws IOException {
+ final String paymentMethodURI = JaxrsResource.PAYMENT_METHODS_PATH + "/" + paymentMethodId;
+ final Response paymentMethodResponse = doGet(paymentMethodURI, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+ assertEquals(paymentMethodResponse.getStatusCode(), Status.OK.getStatusCode());
+
+ final PaymentMethodJson paymentMethodJson = mapper.readValue(paymentMethodResponse.getResponseBody(), PaymentMethodJson.class);
+ assertNotNull(paymentMethodJson);
+
+ return paymentMethodJson;
+ }
+
+ protected PaymentMethodJson getPaymentMethodWithPluginInfo(final String paymentMethodId) throws IOException {
+ final String paymentMethodURI = JaxrsResource.PAYMENT_METHODS_PATH + "/" + paymentMethodId;
+
+ final Map<String, String> queryPaymentMethods = new HashMap<String, String>();
+ queryPaymentMethods.put(QUERY_PAYMENT_METHOD_PLUGIN_INFO, "true");
+ final Response paymentMethodResponse = doGet(paymentMethodURI, queryPaymentMethods, DEFAULT_HTTP_TIMEOUT_SEC);
+ assertEquals(paymentMethodResponse.getStatusCode(), Status.OK.getStatusCode());
+
+ final PaymentMethodJson paymentMethodJson = mapper.readValue(paymentMethodResponse.getResponseBody(), PaymentMethodJson.class);
+ assertNotNull(paymentMethodJson);
+
+ return paymentMethodJson;
+ }
+
+ protected List<PaymentJsonSimple> getPaymentsForAccount(final String accountId) throws IOException {
+ final String paymentsURI = JaxrsResource.ACCOUNTS_PATH + "/" + accountId + "/" + JaxrsResource.PAYMENTS;
+ final Response paymentsResponse = doGet(paymentsURI, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+ assertEquals(paymentsResponse.getStatusCode(), Status.OK.getStatusCode());
+ final String paymentsBaseJson = paymentsResponse.getResponseBody();
+
+ final List<PaymentJsonSimple> paymentJsonSimples = mapper.readValue(paymentsBaseJson, new TypeReference<List<PaymentJsonSimple>>() {});
+ assertNotNull(paymentJsonSimples);
+
+ return paymentJsonSimples;
+ }
+
+ protected List<PaymentJsonSimple> getPaymentsForInvoice(final String invoiceId) throws IOException {
+ final String uri = JaxrsResource.INVOICES_PATH + "/" + invoiceId + "/" + JaxrsResource.PAYMENTS;
+ final Response response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+
+ Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
+ final String baseJson = response.getResponseBody();
+ final List<PaymentJsonSimple> objFromJson = mapper.readValue(baseJson, new TypeReference<List<PaymentJsonSimple>>() {});
+ assertNotNull(objFromJson);
+
+ return objFromJson;
+ }
+
+ protected List<PaymentJsonSimple> createInstaPayment(final AccountJson accountJson, final InvoiceJsonSimple invoice) throws IOException {
+ final PaymentJsonSimple payment = new PaymentJsonSimple(invoice.getAmount(), BigDecimal.ZERO, accountJson.getAccountId(),
+ invoice.getInvoiceId(), null, null, null, null, 0, null, null, null, null, null, null, null);
+ final String postJson = mapper.writeValueAsString(payment);
+
+ final String uri = JaxrsResource.INVOICES_PATH + "/" + invoice.getInvoiceId() + "/" + JaxrsResource.PAYMENTS;
+ doPost(uri, postJson, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+
+ return getPaymentsForInvoice(invoice.getInvoiceId());
+ }
+
+ protected List<PaymentJsonSimple> createExternalPayment(final AccountJson accountJson, final String invoiceId, final BigDecimal paidAmount) throws IOException {
+ final PaymentJsonSimple payment = new PaymentJsonSimple(paidAmount, BigDecimal.ZERO, accountJson.getAccountId(),
+ invoiceId, null, null, null, null, 0,
+ null, null, null, null, null, null, null);
+ final String postJson = mapper.writeValueAsString(payment);
+
+ final String paymentURI = JaxrsResource.INVOICES_PATH + "/" + invoiceId + "/" + JaxrsResource.PAYMENTS;
+ final Response paymentResponse = doPost(paymentURI, postJson, ImmutableMap.<String, String>of("externalPayment", "true"), DEFAULT_HTTP_TIMEOUT_SEC);
+ assertEquals(paymentResponse.getStatusCode(), Status.CREATED.getStatusCode());
+
+ return getPaymentsForInvoice(invoiceId);
+ }
+
+ //
+ // REFUNDS
+ //
+
+ protected List<RefundJson> getRefundsForPayment(final String paymentId) throws IOException {
+ final String uri = JaxrsResource.PAYMENTS_PATH + "/" + paymentId + "/" + JaxrsResource.REFUNDS;
+ final Response response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+
+ assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
+ final String baseJson = response.getResponseBody();
+ final List<RefundJson> refunds = mapper.readValue(baseJson, new TypeReference<List<RefundJson>>() {});
+ assertNotNull(refunds);
+
+ return refunds;
+ }
+
+ protected RefundJson createRefund(final String paymentId, final BigDecimal amount) throws IOException {
+ return doCreateRefund(paymentId, amount, false, ImmutableMap.<String, BigDecimal>of());
+ }
+
+ protected RefundJson createRefundWithInvoiceAdjustment(final String paymentId, final BigDecimal amount) throws IOException {
+ return doCreateRefund(paymentId, amount, true, ImmutableMap.<String, BigDecimal>of());
+ }
+
+ protected RefundJson createRefundWithInvoiceItemAdjustment(final String paymentId, final String invoiceItemId, final BigDecimal amount) throws IOException {
+ final Map<String, BigDecimal> adjustments = new HashMap<String, BigDecimal>();
+ adjustments.put(invoiceItemId, amount);
+ return doCreateRefund(paymentId, amount, true, adjustments);
+ }
+
+ private RefundJson doCreateRefund(final String paymentId, final BigDecimal amount, final boolean adjusted, final Map<String, BigDecimal> itemAdjustments) throws IOException {
+ final String uri = JaxrsResource.PAYMENTS_PATH + "/" + paymentId + "/" + JaxrsResource.REFUNDS;
+
+ final List<InvoiceItemJsonSimple> adjustments = new ArrayList<InvoiceItemJsonSimple>();
+ for (final String itemId : itemAdjustments.keySet()) {
+ adjustments.add(new InvoiceItemJsonSimple(itemId, null, null, null, null, null, null, null, null, null,
+ itemAdjustments.get(itemId), null, null));
+ }
+ final RefundJson refundJson = new RefundJson(null, paymentId, amount, adjusted, null, null, adjustments, null);
+ final String baseJson = mapper.writeValueAsString(refundJson);
+ final Response response = doPost(uri, baseJson, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+ assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
+
+ final String locationCC = response.getHeader("Location");
+ Assert.assertNotNull(locationCC);
+
+ // Retrieves by Id based on Location returned
+ final Response retrievedResponse = doGetWithUrl(locationCC, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+ Assert.assertEquals(retrievedResponse.getStatusCode(), Status.OK.getStatusCode());
+ final String retrievedBaseJson = retrievedResponse.getResponseBody();
+ final RefundJson retrievedRefundJson = mapper.readValue(retrievedBaseJson, RefundJson.class);
+ assertNotNull(retrievedRefundJson);
+
+ return retrievedRefundJson;
+ }
+
protected Map<String, String> getQueryParamsForCallCompletion(final String timeoutSec) {
final Map<String, String> queryParams = new HashMap<String, String>();
queryParams.put(JaxrsResource.QUERY_CALL_COMPLETION, "true");
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java b/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java
index 2b16d3f..14c4ae9 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java
@@ -16,96 +16,160 @@
package com.ning.billing.jaxrs;
+import java.io.IOException;
import java.math.BigDecimal;
-import java.util.HashMap;
+import java.math.RoundingMode;
import java.util.List;
-import java.util.Map;
-
-import javax.ws.rs.core.Response.Status;
import org.testng.Assert;
import org.testng.annotations.Test;
-import com.ning.billing.catalog.api.BillingPeriod;
-import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.jaxrs.json.AccountJson;
-import com.ning.billing.jaxrs.json.BundleJsonNoSubscriptions;
+import com.ning.billing.jaxrs.json.InvoiceItemJsonSimple;
+import com.ning.billing.jaxrs.json.InvoiceJsonSimple;
+import com.ning.billing.jaxrs.json.InvoiceJsonWithItems;
import com.ning.billing.jaxrs.json.PaymentJsonSimple;
import com.ning.billing.jaxrs.json.PaymentMethodJson;
import com.ning.billing.jaxrs.json.RefundJson;
-import com.ning.billing.jaxrs.json.SubscriptionJsonNoEvents;
-import com.ning.billing.jaxrs.resources.JaxrsResource;
-import com.ning.http.client.Response;
-import com.fasterxml.jackson.core.type.TypeReference;
+public class TestPayment extends TestJaxrsBase {
+
+ @Test(groups = "slow")
+ public void testFullRefundWithNoAdjustment() throws Exception {
+ final PaymentJsonSimple paymentJsonSimple = setupScenarioWithPayment();
-import static com.ning.billing.jaxrs.resources.JaxrsResource.QUERY_PAYMENT_METHOD_PLUGIN_INFO;
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
+ // Issue a refund for the full amount
+ final BigDecimal refundAmount = paymentJsonSimple.getAmount();
+ final BigDecimal expectedInvoiceBalance = refundAmount;
-public class TestPayment extends TestJaxrsBase {
+ // Post and verify the refund
+ final RefundJson refundJsonCheck = createRefund(paymentJsonSimple.getPaymentId(), refundAmount);
+ verifyRefund(paymentJsonSimple, refundJsonCheck, refundAmount);
+
+ // Verify the invoice balance
+ verifyInvoice(paymentJsonSimple, expectedInvoiceBalance);
+ }
+
+ @Test(groups = "slow")
+ public void testPartialRefundWithNoAdjustment() throws Exception {
+ final PaymentJsonSimple paymentJsonSimple = setupScenarioWithPayment();
+
+ // Issue a refund for a fraction of the amount
+ final BigDecimal refundAmount = getFractionOfAmount(paymentJsonSimple.getAmount());
+ final BigDecimal expectedInvoiceBalance = refundAmount;
+
+ // Post and verify the refund
+ final RefundJson refundJsonCheck = createRefund(paymentJsonSimple.getPaymentId(), refundAmount);
+ verifyRefund(paymentJsonSimple, refundJsonCheck, refundAmount);
+
+ // Verify the invoice balance
+ verifyInvoice(paymentJsonSimple, expectedInvoiceBalance);
+ }
+
+ @Test(groups = "slow")
+ public void testFullRefundWithInvoiceAdjustment() throws Exception {
+ final PaymentJsonSimple paymentJsonSimple = setupScenarioWithPayment();
+
+ // Issue a refund for the full amount
+ final BigDecimal refundAmount = paymentJsonSimple.getAmount();
+ final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO;
+
+ // Post and verify the refund
+ final RefundJson refundJsonCheck = createRefundWithInvoiceAdjustment(paymentJsonSimple.getPaymentId(), refundAmount);
+ verifyRefund(paymentJsonSimple, refundJsonCheck, refundAmount);
+
+ // Verify the invoice balance
+ verifyInvoice(paymentJsonSimple, expectedInvoiceBalance);
+ }
+
+ @Test(groups = "slow")
+ public void testPartialRefundWithInvoiceAdjustment() throws Exception {
+ final PaymentJsonSimple paymentJsonSimple = setupScenarioWithPayment();
+
+ // Issue a refund for a fraction of the amount
+ final BigDecimal refundAmount = getFractionOfAmount(paymentJsonSimple.getAmount());
+ final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO;
+
+ // Post and verify the refund
+ final RefundJson refundJsonCheck = createRefundWithInvoiceAdjustment(paymentJsonSimple.getPaymentId(), refundAmount);
+ verifyRefund(paymentJsonSimple, refundJsonCheck, refundAmount);
+
+ // Verify the invoice balance
+ verifyInvoice(paymentJsonSimple, expectedInvoiceBalance);
+ }
@Test(groups = "slow")
- public void testPaymentWithRefund() throws Exception {
- final AccountJson accountJson = createAccountWithDefaultPaymentMethod("eraahahildo", "sheqrgfhwe", "eraahahildo@yahoo.com");
- assertNotNull(accountJson);
+ public void testRefundWithFullInvoiceItemAdjustment() throws Exception {
+ final PaymentJsonSimple paymentJsonSimple = setupScenarioWithPayment();
- final BundleJsonNoSubscriptions bundleJson = createBundle(accountJson.getAccountId(), "317199");
- assertNotNull(bundleJson);
+ // Get the individual items for the invoice
+ final InvoiceJsonWithItems invoice = getInvoiceWithItems(paymentJsonSimple.getInvoiceId());
+ final InvoiceItemJsonSimple itemToAdjust = invoice.getItems().get(0);
- final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), "Shotgun", ProductCategory.BASE.toString(), BillingPeriod.MONTHLY.toString(), true);
- assertNotNull(subscriptionJson);
+ // Issue a refund for the full amount
+ final BigDecimal refundAmount = itemToAdjust.getAmount();
+ final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO;
- clock.addMonths(1);
- crappyWaitForLackOfProperSynchonization();
+ // Post and verify the refund
+ final RefundJson refundJsonCheck = createRefundWithInvoiceItemAdjustment(paymentJsonSimple.getPaymentId(),
+ itemToAdjust.getInvoiceItemId(),
+ null /* null means full adjustment for that item */);
+ verifyRefund(paymentJsonSimple, refundJsonCheck, refundAmount);
- String uri = JaxrsResource.ACCOUNTS_PATH + "/" + accountJson.getAccountId() + "/" + JaxrsResource.PAYMENTS;
+ // Verify the invoice balance
+ verifyInvoice(paymentJsonSimple, expectedInvoiceBalance);
+ }
- Response response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
- Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- String baseJson = response.getResponseBody();
- final List<PaymentJsonSimple> objFromJson = mapper.readValue(baseJson, new TypeReference<List<PaymentJsonSimple>>() {});
- Assert.assertEquals(objFromJson.size(), 1);
+ @Test(groups = "slow")
+ public void testPartialRefundWithInvoiceItemAdjustment() throws Exception {
+ final PaymentJsonSimple paymentJsonSimple = setupScenarioWithPayment();
- final String paymentId = objFromJson.get(0).getPaymentId();
- final BigDecimal paymentAmount = objFromJson.get(0).getAmount();
+ // Get the individual items for the invoice
+ final InvoiceJsonWithItems invoice = getInvoiceWithItems(paymentJsonSimple.getInvoiceId());
+ final InvoiceItemJsonSimple itemToAdjust = invoice.getItems().get(0);
+
+ // Issue a refund for a fraction of the amount
+ final BigDecimal refundAmount = getFractionOfAmount(itemToAdjust.getAmount());
+ final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO;
+
+ // Post and verify the refund
+ final RefundJson refundJsonCheck = createRefundWithInvoiceItemAdjustment(paymentJsonSimple.getPaymentId(),
+ itemToAdjust.getInvoiceItemId(),
+ refundAmount);
+ verifyRefund(paymentJsonSimple, refundJsonCheck, refundAmount);
+
+ // Verify the invoice balance
+ verifyInvoice(paymentJsonSimple, expectedInvoiceBalance);
+ }
+
+ private BigDecimal getFractionOfAmount(final BigDecimal amount) {
+ return amount.divide(BigDecimal.TEN).setScale(2, BigDecimal.ROUND_HALF_UP);
+ }
+
+ private PaymentJsonSimple setupScenarioWithPayment() throws Exception {
+ final AccountJson accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ final List<PaymentJsonSimple> firstPaymentForAccount = getPaymentsForAccount(accountJson.getAccountId());
+ Assert.assertEquals(firstPaymentForAccount.size(), 1);
+
+ final PaymentJsonSimple paymentJsonSimple = firstPaymentForAccount.get(0);
// Check the PaymentMethod from paymentMethodId returned in the Payment object
- final String paymentMethodId = objFromJson.get(0).getPaymentMethodId();
- uri = JaxrsResource.PAYMENT_METHODS_PATH + "/" + paymentMethodId;
-
- final Map<String, String> queryPaymentMethods = new HashMap<String, String>();
- queryPaymentMethods.put(QUERY_PAYMENT_METHOD_PLUGIN_INFO, "true");
- response = doGet(uri, queryPaymentMethods, DEFAULT_HTTP_TIMEOUT_SEC);
- Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- final PaymentMethodJson paymentMethodJson = mapper.readValue(response.getResponseBody(), PaymentMethodJson.class);
+ final String paymentMethodId = paymentJsonSimple.getPaymentMethodId();
+ final PaymentMethodJson paymentMethodJson = getPaymentMethodWithPluginInfo(paymentMethodId);
Assert.assertEquals(paymentMethodJson.getPaymentMethodId(), paymentMethodId);
Assert.assertEquals(paymentMethodJson.getAccountId(), accountJson.getAccountId());
Assert.assertNotNull(paymentMethodJson.getPluginInfo().getExternalPaymentId());
- uri = JaxrsResource.PAYMENTS_PATH + "/" + paymentId + "/" + JaxrsResource.REFUNDS;
- response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
- Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- baseJson = response.getResponseBody();
- List<RefundJson> objRefundFromJson = mapper.readValue(baseJson, new TypeReference<List<RefundJson>>() {});
+ // Verify the refunds
+ final List<RefundJson> objRefundFromJson = getRefundsForPayment(paymentJsonSimple.getPaymentId());
Assert.assertEquals(objRefundFromJson.size(), 0);
+ return paymentJsonSimple;
+ }
- // Issue the refund
-
- final RefundJson refundJson = new RefundJson(null, paymentId, paymentAmount, false, null, null, null);
- baseJson = mapper.writeValueAsString(refundJson);
- response = doPost(uri, baseJson, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
- assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
-
- final String locationCC = response.getHeader("Location");
- Assert.assertNotNull(locationCC);
-
- // Retrieves by Id based on Location returned
- response = doGetWithUrl(locationCC, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
- Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- baseJson = response.getResponseBody();
- final RefundJson refundJsonCheck = mapper.readValue(baseJson, RefundJson.class);
- Assert.assertTrue(refundJsonCheck.equalsNoIdNoDates(refundJson));
+ private void verifyRefund(final PaymentJsonSimple paymentJsonSimple, final RefundJson refundJsonCheck, final BigDecimal refundAmount) throws IOException {
+ Assert.assertEquals(refundJsonCheck.getPaymentId(), paymentJsonSimple.getPaymentId());
+ Assert.assertEquals(refundJsonCheck.getRefundAmount().setScale(2, RoundingMode.HALF_UP), refundAmount.setScale(2, RoundingMode.HALF_UP));
Assert.assertEquals(refundJsonCheck.getEffectiveDate().getYear(), clock.getUTCNow().getYear());
Assert.assertEquals(refundJsonCheck.getEffectiveDate().getMonthOfYear(), clock.getUTCNow().getMonthOfYear());
Assert.assertEquals(refundJsonCheck.getEffectiveDate().getDayOfMonth(), clock.getUTCNow().getDayOfMonth());
@@ -113,11 +177,14 @@ public class TestPayment extends TestJaxrsBase {
Assert.assertEquals(refundJsonCheck.getRequestedDate().getMonthOfYear(), clock.getUTCNow().getMonthOfYear());
Assert.assertEquals(refundJsonCheck.getRequestedDate().getDayOfMonth(), clock.getUTCNow().getDayOfMonth());
- uri = JaxrsResource.PAYMENTS_PATH + "/" + paymentId + "/" + JaxrsResource.REFUNDS;
- response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
- Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
- baseJson = response.getResponseBody();
- objRefundFromJson = mapper.readValue(baseJson, new TypeReference<List<RefundJson>>() {});
- Assert.assertEquals(objRefundFromJson.size(), 1);
+ // Verify the refunds
+ final List<RefundJson> retrievedRefunds = getRefundsForPayment(paymentJsonSimple.getPaymentId());
+ Assert.assertEquals(retrievedRefunds.size(), 1);
+ }
+
+ private void verifyInvoice(final PaymentJsonSimple paymentJsonSimple, final BigDecimal expectedInvoiceBalance) throws IOException {
+ final InvoiceJsonSimple invoiceJsonSimple = getInvoice(paymentJsonSimple.getInvoiceId());
+ Assert.assertEquals(invoiceJsonSimple.getBalance().setScale(2, BigDecimal.ROUND_HALF_UP),
+ expectedInvoiceBalance.setScale(2, BigDecimal.ROUND_HALF_UP));
}
}
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestSubscription.java b/server/src/test/java/com/ning/billing/jaxrs/TestSubscription.java
index da9b666..9465e90 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestSubscription.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestSubscription.java
@@ -23,6 +23,7 @@ import javax.ws.rs.core.Response.Status;
import org.joda.time.DateTime;
import org.joda.time.Interval;
+import org.joda.time.LocalDate;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -57,7 +58,7 @@ public class TestSubscription extends TestJaxrsBase {
final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), productName, ProductCategory.BASE.toString(), term.toString(), true);
Assert.assertNotNull(subscriptionJson.getChargedThroughDate());
- Assert.assertEquals(subscriptionJson.getChargedThroughDate().toString(), "2012-04-25T00:00:00.000Z");
+ Assert.assertEquals(subscriptionJson.getChargedThroughDate().toLocalDate(), new LocalDate("2012-04-25"));
String uri = JaxrsResource.SUBSCRIPTIONS_PATH + "/" + subscriptionJson.getSubscriptionId();
@@ -144,7 +145,7 @@ public class TestSubscription extends TestJaxrsBase {
final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), productName, ProductCategory.BASE.toString(), term.toString(), true);
Assert.assertNotNull(subscriptionJson.getChargedThroughDate());
- Assert.assertEquals(subscriptionJson.getChargedThroughDate().toString(), "2012-04-25T00:00:00.000Z");
+ Assert.assertEquals(subscriptionJson.getChargedThroughDate().toLocalDate(), new LocalDate("2012-04-25"));
final String uri = JaxrsResource.SUBSCRIPTIONS_PATH + "/" + subscriptionJson.getSubscriptionId();