Details
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java
index 092d2b5..9aa8eb1 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java
@@ -41,6 +41,7 @@ import org.killbill.billing.mock.MockAccountBuilder;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.usage.api.SubscriptionUsageRecord;
import org.killbill.billing.usage.api.UnitUsageRecord;
+import org.killbill.billing.usage.api.UsageApiException;
import org.killbill.billing.usage.api.UsageRecord;
import org.killbill.billing.util.callcontext.CallContext;
import org.skife.jdbi.v2.Handle;
@@ -245,12 +246,12 @@ public class TestConsumableInArrear extends TestIntegrationBase {
Assert.assertEquals(countNotifications.intValue(), 4);
}
- private void setUsage(final UUID subscriptionId, final String unitType, final LocalDate startDate, final Long amount, final CallContext context) {
+ private void setUsage(final UUID subscriptionId, final String unitType, final LocalDate startDate, final Long amount, final CallContext context) throws UsageApiException {
final List<UsageRecord> usageRecords = new ArrayList<UsageRecord>();
usageRecords.add(new UsageRecord(startDate, amount));
final List<UnitUsageRecord> unitUsageRecords = new ArrayList<UnitUsageRecord>();
unitUsageRecords.add(new UnitUsageRecord(unitType, usageRecords));
- final SubscriptionUsageRecord record = new SubscriptionUsageRecord(subscriptionId, unitUsageRecords);
+ final SubscriptionUsageRecord record = new SubscriptionUsageRecord(subscriptionId, UUID.randomUUID().toString(), unitUsageRecords);
usageUserApi.recordRolledUpUsage(record, context);
}
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionUsageRecordJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionUsageRecordJson.java
index 9e961a6..9a38ad5 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionUsageRecordJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionUsageRecordJson.java
@@ -28,6 +28,7 @@ import org.killbill.billing.usage.api.UsageRecord;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Function;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.wordnik.swagger.annotations.ApiModelProperty;
@@ -38,11 +39,15 @@ public class SubscriptionUsageRecordJson {
private final String subscriptionId;
@ApiModelProperty(required = true)
private final List<UnitUsageRecordJson> unitUsageRecords;
+ @ApiModelProperty(required = false)
+ private final String trackingId;
@JsonCreator
public SubscriptionUsageRecordJson(@JsonProperty("subscriptionId") final String subscriptionId,
+ @JsonProperty("trackingId") final String trackingId,
@JsonProperty("unitUsageRecords") final List<UnitUsageRecordJson> unitUsageRecords) {
this.subscriptionId = subscriptionId;
+ this.trackingId = trackingId;
this.unitUsageRecords = unitUsageRecords;
}
@@ -54,6 +59,10 @@ public class SubscriptionUsageRecordJson {
return unitUsageRecords;
}
+ public String getTrackingId() {
+ return trackingId;
+ }
+
public static class UnitUsageRecordJson {
private final String unitType;
@@ -117,7 +126,7 @@ public class SubscriptionUsageRecordJson {
return input.toUnitUsageRecord();
}
}));
- final SubscriptionUsageRecord result = new SubscriptionUsageRecord(UUID.fromString(subscriptionId), tmp);
+ final SubscriptionUsageRecord result = new SubscriptionUsageRecord(UUID.fromString(subscriptionId), trackingId, tmp);
return result;
}
-}
+}
\ No newline at end of file
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/UsageResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/UsageResource.java
index 6265979..ea8271a 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/UsageResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/UsageResource.java
@@ -47,6 +47,7 @@ import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
import org.killbill.billing.payment.api.PaymentApi;
import org.killbill.billing.usage.api.RolledUpUsage;
import org.killbill.billing.usage.api.SubscriptionUsageRecord;
+import org.killbill.billing.usage.api.UsageApiException;
import org.killbill.billing.usage.api.UsageUserApi;
import org.killbill.billing.util.api.AuditUserApi;
import org.killbill.billing.util.api.CustomFieldUserApi;
@@ -101,7 +102,9 @@ public class UsageResource extends JaxRsResourceBase {
@HeaderParam(HDR_REASON) final String reason,
@HeaderParam(HDR_COMMENT) final String comment,
@javax.ws.rs.core.Context final HttpServletRequest request,
- @javax.ws.rs.core.Context final UriInfo uriInfo) throws EntitlementApiException, AccountApiException {
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws EntitlementApiException,
+ AccountApiException,
+ UsageApiException {
verifyNonNullOrEmpty(json, "SubscriptionUsageRecordJson body should be specified");
verifyNonNullOrEmpty(json.getSubscriptionId(), "SubscriptionUsageRecordJson subscriptionId needs to be set",
json.getUnitUsageRecords(), "SubscriptionUsageRecordJson unitUsageRecords needs to be set");
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestSubscriptionUsageRecordJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestSubscriptionUsageRecordJson.java
new file mode 100644
index 0000000..95c232b
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestSubscriptionUsageRecordJson.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+import org.killbill.billing.jaxrs.json.SubscriptionUsageRecordJson.UnitUsageRecordJson;
+import org.killbill.billing.jaxrs.json.SubscriptionUsageRecordJson.UsageRecordJson;
+import org.killbill.billing.usage.api.SubscriptionUsageRecord;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class TestSubscriptionUsageRecordJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final LocalDate localDate = new LocalDate();
+ final String subscriptionId = UUID.randomUUID().toString();
+ final String trackingId = UUID.randomUUID().toString();
+ final List<UnitUsageRecordJson> unitUsageRecords = new ArrayList<UnitUsageRecordJson>();
+ final List<UsageRecordJson> usageRecords = new ArrayList<UsageRecordJson>();
+ final UsageRecordJson usageRecordJson = new UsageRecordJson(localDate, 5L);
+ usageRecords.add(usageRecordJson);
+ final UnitUsageRecordJson unitUsageRecordJson = new UnitUsageRecordJson("foo", usageRecords);
+ unitUsageRecords.add(unitUsageRecordJson);
+
+ final SubscriptionUsageRecordJson subscriptionUsageRecordJson = new SubscriptionUsageRecordJson(subscriptionId, trackingId, unitUsageRecords);
+ Assert.assertEquals(subscriptionUsageRecordJson.getSubscriptionId(), subscriptionId);
+ Assert.assertEquals(subscriptionUsageRecordJson.getTrackingId(), trackingId);
+ Assert.assertEquals(subscriptionUsageRecordJson.getUnitUsageRecords().size(), 1);
+ Assert.assertEquals(subscriptionUsageRecordJson.getUnitUsageRecords().get(0).getUnitType(), "foo");
+ Assert.assertEquals(subscriptionUsageRecordJson.getUnitUsageRecords().get(0).getUsageRecords().size(), 1);
+ Assert.assertEquals(subscriptionUsageRecordJson.getUnitUsageRecords().get(0).getUsageRecords().get(0).getAmount(), new Long(5L));
+ Assert.assertEquals(subscriptionUsageRecordJson.getUnitUsageRecords().get(0).getUsageRecords().get(0).getRecordDate(), localDate);
+
+ final SubscriptionUsageRecord subscriptionUsageRecord = subscriptionUsageRecordJson.toSubscriptionUsageRecord();
+ Assert.assertEquals(subscriptionUsageRecord.getSubscriptionId().toString(), subscriptionId);
+ Assert.assertEquals(subscriptionUsageRecord.getTrackingId(), trackingId);
+ Assert.assertEquals(subscriptionUsageRecord.getUnitUsageRecord().size(), 1);
+ Assert.assertEquals(subscriptionUsageRecord.getUnitUsageRecord().get(0).getUnitType(), "foo");
+ Assert.assertEquals(subscriptionUsageRecord.getUnitUsageRecord().get(0).getDailyAmount().size(), 1);
+ Assert.assertEquals(subscriptionUsageRecord.getUnitUsageRecord().get(0).getDailyAmount().get(0).getAmount(), new Long(5L));
+ Assert.assertEquals(subscriptionUsageRecord.getUnitUsageRecord().get(0).getDailyAmount().get(0).getDate(), localDate);
+ }
+}
\ No newline at end of file
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
index 14e7ab0..1e7962a 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
@@ -1,7 +1,8 @@
/*
- * Copyright 2014 Groupon, Inc
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
*
- * Groupon licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
@@ -17,6 +18,8 @@
package org.killbill.billing.payment.api;
import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@@ -28,6 +31,7 @@ import org.killbill.billing.entity.EntityBase;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
public class DefaultPayment extends EntityBase implements Payment {
@@ -40,7 +44,7 @@ public class DefaultPayment extends EntityBase implements Payment {
private final BigDecimal purchasedAmount;
private final BigDecimal creditAmount;
private final BigDecimal refundAmount;
- private final Boolean isVoided;
+ private final Boolean isAuthVoided;
private final Currency currency;
private final List<PaymentTransaction> transactions;
@@ -56,18 +60,40 @@ public class DefaultPayment extends EntityBase implements Payment {
this.paymentNumber = paymentNumber;
this.externalKey = externalKey;
this.transactions = transactions;
- this.authAmount = getAmountForType(transactions, TransactionType.AUTHORIZE);
- this.captureAmount = getAmountForType(transactions, TransactionType.CAPTURE);
- this.purchasedAmount = getAmountForType(transactions, TransactionType.PURCHASE);
- this.creditAmount = getAmountForType(transactions, TransactionType.CREDIT);
- this.refundAmount = getAmountForType(transactions, TransactionType.REFUND);
- this.isVoided = Iterables.filter(transactions, new Predicate<PaymentTransaction>() {
- @Override
- public boolean apply(final PaymentTransaction input) {
- return input.getTransactionType() == TransactionType.VOID && TransactionStatus.SUCCESS.equals(input.getTransactionStatus());
+
+ final Collection<PaymentTransaction> voidedTransactions = new LinkedList<PaymentTransaction>();
+ final Collection<PaymentTransaction> nonVoidedTransactions = new LinkedList<PaymentTransaction>();
+ int nvTxToVoid = 0;
+ for (final PaymentTransaction paymentTransaction : Lists.<PaymentTransaction>reverse(transactions)) {
+ if (TransactionStatus.SUCCESS.equals(paymentTransaction.getTransactionStatus())) {
+ if (paymentTransaction.getTransactionType() == TransactionType.VOID) {
+ nvTxToVoid++;
+ } else {
+ if (nvTxToVoid > 0) {
+ nvTxToVoid--;
+ voidedTransactions.add(paymentTransaction);
+ } else {
+ nonVoidedTransactions.add(paymentTransaction);
+ }
+ }
}
- }).iterator().hasNext();
- this.currency = (transactions != null && !transactions.isEmpty()) ? transactions.get(0).getCurrency() : null;
+ }
+
+ this.authAmount = getAmountForType(nonVoidedTransactions, TransactionType.AUTHORIZE);
+ this.captureAmount = getAmountForType(nonVoidedTransactions, TransactionType.CAPTURE);
+ this.purchasedAmount = getAmountForType(nonVoidedTransactions, TransactionType.PURCHASE);
+ this.creditAmount = getAmountForType(nonVoidedTransactions, TransactionType.CREDIT);
+ this.refundAmount = getAmountForType(nonVoidedTransactions, TransactionType.REFUND);
+
+ this.isAuthVoided = Iterables.<PaymentTransaction>tryFind(voidedTransactions,
+ new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction input) {
+ return input.getTransactionType() == TransactionType.AUTHORIZE && TransactionStatus.SUCCESS.equals(input.getTransactionStatus());
+ }
+ }).isPresent();
+
+ this.currency = !transactions.isEmpty() ? transactions.get(0).getCurrency() : null;
}
private static BigDecimal getAmountForType(final Iterable<PaymentTransaction> transactions, final TransactionType transactiontype) {
@@ -141,7 +167,7 @@ public class DefaultPayment extends EntityBase implements Payment {
@Override
public Boolean isAuthVoided() {
- return isVoided;
+ return isAuthVoided;
}
@Override
diff --git a/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml b/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
index ebfa2d2..14146d6 100644
--- a/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
+++ b/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
@@ -472,6 +472,30 @@
<finalState>VOID_INIT</finalState>
</linkStateMachine>
<linkStateMachine>
+ <initialStateMachine>VOID</initialStateMachine>
+ <initialState>VOID_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>VOID</initialStateMachine>
+ <initialState>VOID_SUCCESS</initialState>
+ <finalStateMachine>CAPTURE</finalStateMachine>
+ <finalState>CAPTURE_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>VOID</initialStateMachine>
+ <initialState>VOID_SUCCESS</initialState>
+ <finalStateMachine>REFUND</finalStateMachine>
+ <finalState>REFUND_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>VOID</initialStateMachine>
+ <initialState>VOID_SUCCESS</initialState>
+ <finalStateMachine>CREDIT</finalStateMachine>
+ <finalState>CREDIT_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
<initialStateMachine>CAPTURE</initialStateMachine>
<initialState>CAPTURE_SUCCESS</initialState>
<finalStateMachine>REFUND</finalStateMachine>
@@ -490,6 +514,12 @@
<finalState>CHARGEBACK_INIT</finalState>
</linkStateMachine>
<linkStateMachine>
+ <initialStateMachine>CAPTURE</initialStateMachine>
+ <initialState>CAPTURE_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
<initialStateMachine>REFUND</initialStateMachine>
<initialState>REFUND_SUCCESS</initialState>
<finalStateMachine>REFUND</finalStateMachine>
@@ -502,6 +532,12 @@
<finalState>CHARGEBACK_INIT</finalState>
</linkStateMachine>
<linkStateMachine>
+ <initialStateMachine>REFUND</initialStateMachine>
+ <initialState>REFUND_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
<initialStateMachine>PURCHASE</initialStateMachine>
<initialState>PURCHASE_SUCCESS</initialState>
<finalStateMachine>REFUND</finalStateMachine>
@@ -514,6 +550,12 @@
<finalState>CHARGEBACK_INIT</finalState>
</linkStateMachine>
<linkStateMachine>
+ <initialStateMachine>CREDIT</initialStateMachine>
+ <initialState>CREDIT_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
<initialStateMachine>CHARGEBACK</initialStateMachine>
<initialState>CHARGEBACK_SUCCESS</initialState>
<finalStateMachine>CHARGEBACK</finalStateMachine>
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
index 8b36812..a3cca2f 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
@@ -59,6 +59,7 @@ import com.google.common.collect.ImmutableList;
import static org.killbill.billing.payment.logging.TestLoggingHelper.withSpyLogger;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
@@ -256,16 +257,80 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
@Test(groups = "slow")
- public void testCreateSuccessAuthCapture() throws PaymentApiException {
+ public void testCreateSuccessAuthVoid() throws PaymentApiException {
+ final BigDecimal authAmount = BigDecimal.TEN;
+
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final String transactionExternalKey = UUID.randomUUID().toString();
+ final String transactionExternalKey2 = UUID.randomUUID().toString();
+
+ final Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, authAmount, Currency.AED,
+ paymentExternalKey, transactionExternalKey,
+ ImmutableList.<PluginProperty>of(), callContext);
+
+ assertEquals(payment.getExternalKey(), paymentExternalKey);
+ assertEquals(payment.getPaymentMethodId(), account.getPaymentMethodId());
+ assertEquals(payment.getAccountId(), account.getId());
+ assertEquals(payment.getAuthAmount().compareTo(authAmount), 0);
+ assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment.getCurrency(), Currency.AED);
+ assertFalse(payment.isAuthVoided());
+
+ assertEquals(payment.getTransactions().size(), 1);
+ assertEquals(payment.getTransactions().get(0).getExternalKey(), transactionExternalKey);
+ assertEquals(payment.getTransactions().get(0).getPaymentId(), payment.getId());
+ assertEquals(payment.getTransactions().get(0).getAmount().compareTo(authAmount), 0);
+ assertEquals(payment.getTransactions().get(0).getCurrency(), Currency.AED);
+ assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(authAmount), 0);
+ assertEquals(payment.getTransactions().get(0).getProcessedCurrency(), Currency.AED);
+
+ assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payment.getTransactions().get(0).getTransactionType(), TransactionType.AUTHORIZE);
+ assertNotNull(payment.getTransactions().get(0).getGatewayErrorMsg());
+ assertNotNull(payment.getTransactions().get(0).getGatewayErrorCode());
+
+ // Void the authorization
+ final Payment payment2 = paymentApi.createVoid(account, payment.getId(), transactionExternalKey2, ImmutableList.<PluginProperty>of(), callContext);
+
+ assertEquals(payment2.getExternalKey(), paymentExternalKey);
+ assertEquals(payment2.getPaymentMethodId(), account.getPaymentMethodId());
+ assertEquals(payment2.getAccountId(), account.getId());
+ assertEquals(payment2.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment2.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment2.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment2.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment2.getCurrency(), Currency.AED);
+ assertTrue(payment2.isAuthVoided());
+ assertEquals(payment2.getTransactions().size(), 2);
+ assertEquals(payment2.getTransactions().get(1).getExternalKey(), transactionExternalKey2);
+ assertEquals(payment2.getTransactions().get(1).getPaymentId(), payment.getId());
+ assertNull(payment2.getTransactions().get(1).getAmount());
+ assertNull(payment2.getTransactions().get(1).getCurrency());
+ assertNull(payment2.getTransactions().get(1).getProcessedAmount());
+ assertNull(payment2.getTransactions().get(1).getProcessedCurrency());
+
+ assertEquals(payment2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payment2.getTransactions().get(1).getTransactionType(), TransactionType.VOID);
+ assertNotNull(payment2.getTransactions().get(1).getGatewayErrorMsg());
+ assertNotNull(payment2.getTransactions().get(1).getGatewayErrorCode());
+ }
+
+ @Test(groups = "slow")
+ public void testCreateSuccessAuthCaptureVoidCapture() throws PaymentApiException {
final BigDecimal authAmount = BigDecimal.TEN;
final BigDecimal captureAmount = BigDecimal.ONE;
- final String paymentExternalKey = "bouzou";
- final String transactionExternalKey = "kaput";
- final String transactionExternalKey2 = "kapu2t";
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final String transactionExternalKey = UUID.randomUUID().toString();
+ final String transactionExternalKey2 = UUID.randomUUID().toString();
+ final String transactionExternalKey3 = UUID.randomUUID().toString();
+ final String transactionExternalKey4 = UUID.randomUUID().toString();
- final Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, authAmount, Currency.AED, paymentExternalKey, transactionExternalKey,
+ final Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, authAmount, Currency.AED,
+ paymentExternalKey, transactionExternalKey,
ImmutableList.<PluginProperty>of(), callContext);
assertEquals(payment.getExternalKey(), paymentExternalKey);
@@ -276,6 +341,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
assertEquals(payment.getCurrency(), Currency.AED);
+ assertFalse(payment.isAuthVoided());
assertEquals(payment.getTransactions().size(), 1);
assertEquals(payment.getTransactions().get(0).getExternalKey(), transactionExternalKey);
@@ -301,6 +367,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(payment2.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
assertEquals(payment2.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
assertEquals(payment2.getCurrency(), Currency.AED);
+ assertFalse(payment2.isAuthVoided());
assertEquals(payment2.getTransactions().size(), 2);
assertEquals(payment2.getTransactions().get(1).getExternalKey(), transactionExternalKey2);
@@ -314,6 +381,176 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(payment2.getTransactions().get(1).getTransactionType(), TransactionType.CAPTURE);
assertNotNull(payment2.getTransactions().get(1).getGatewayErrorMsg());
assertNotNull(payment2.getTransactions().get(1).getGatewayErrorCode());
+
+ // Void the capture
+ final Payment payment3 = paymentApi.createVoid(account, payment.getId(), transactionExternalKey3, ImmutableList.<PluginProperty>of(), callContext);
+
+ assertEquals(payment3.getExternalKey(), paymentExternalKey);
+ assertEquals(payment3.getPaymentMethodId(), account.getPaymentMethodId());
+ assertEquals(payment3.getAccountId(), account.getId());
+ assertEquals(payment3.getAuthAmount().compareTo(authAmount), 0);
+ assertEquals(payment3.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment3.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment3.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment3.getCurrency(), Currency.AED);
+ assertFalse(payment3.isAuthVoided());
+
+ assertEquals(payment3.getTransactions().size(), 3);
+ assertEquals(payment3.getTransactions().get(2).getExternalKey(), transactionExternalKey3);
+ assertEquals(payment3.getTransactions().get(2).getPaymentId(), payment.getId());
+ assertNull(payment3.getTransactions().get(2).getAmount());
+ assertNull(payment3.getTransactions().get(2).getCurrency());
+ assertNull(payment3.getTransactions().get(2).getProcessedAmount());
+ assertNull(payment3.getTransactions().get(2).getProcessedCurrency());
+
+ assertEquals(payment3.getTransactions().get(2).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payment3.getTransactions().get(2).getTransactionType(), TransactionType.VOID);
+ assertNotNull(payment3.getTransactions().get(2).getGatewayErrorMsg());
+ assertNotNull(payment3.getTransactions().get(2).getGatewayErrorCode());
+
+ // Capture again
+ final Payment payment4 = paymentApi.createCapture(account, payment.getId(), captureAmount, Currency.AED, transactionExternalKey4,
+ ImmutableList.<PluginProperty>of(), callContext);
+
+ assertEquals(payment4.getExternalKey(), paymentExternalKey);
+ assertEquals(payment4.getPaymentMethodId(), account.getPaymentMethodId());
+ assertEquals(payment4.getAccountId(), account.getId());
+ assertEquals(payment4.getAuthAmount().compareTo(authAmount), 0);
+ assertEquals(payment4.getCapturedAmount().compareTo(captureAmount), 0);
+ assertEquals(payment4.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment4.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment4.getCurrency(), Currency.AED);
+ assertFalse(payment4.isAuthVoided());
+
+ assertEquals(payment4.getTransactions().size(), 4);
+ assertEquals(payment4.getTransactions().get(3).getExternalKey(), transactionExternalKey4);
+ assertEquals(payment4.getTransactions().get(3).getPaymentId(), payment.getId());
+ assertEquals(payment4.getTransactions().get(3).getAmount().compareTo(captureAmount), 0);
+ assertEquals(payment4.getTransactions().get(3).getCurrency(), Currency.AED);
+ assertEquals(payment4.getTransactions().get(3).getProcessedAmount().compareTo(captureAmount), 0);
+ assertEquals(payment4.getTransactions().get(3).getProcessedCurrency(), Currency.AED);
+
+ assertEquals(payment4.getTransactions().get(3).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payment4.getTransactions().get(3).getTransactionType(), TransactionType.CAPTURE);
+ assertNotNull(payment4.getTransactions().get(3).getGatewayErrorMsg());
+ assertNotNull(payment4.getTransactions().get(3).getGatewayErrorCode());
+ }
+
+ @Test(groups = "slow")
+ public void testCreateSuccessAuthCaptureVoidVoid() throws PaymentApiException {
+ final BigDecimal authAmount = BigDecimal.TEN;
+ final BigDecimal captureAmount = BigDecimal.ONE;
+
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final String transactionExternalKey = UUID.randomUUID().toString();
+ final String transactionExternalKey2 = UUID.randomUUID().toString();
+ final String transactionExternalKey3 = UUID.randomUUID().toString();
+ final String transactionExternalKey4 = UUID.randomUUID().toString();
+
+ final Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, authAmount, Currency.AED,
+ paymentExternalKey, transactionExternalKey,
+ ImmutableList.<PluginProperty>of(), callContext);
+
+ assertEquals(payment.getExternalKey(), paymentExternalKey);
+ assertEquals(payment.getPaymentMethodId(), account.getPaymentMethodId());
+ assertEquals(payment.getAccountId(), account.getId());
+ assertEquals(payment.getAuthAmount().compareTo(authAmount), 0);
+ assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment.getCurrency(), Currency.AED);
+ assertFalse(payment.isAuthVoided());
+
+ assertEquals(payment.getTransactions().size(), 1);
+ assertEquals(payment.getTransactions().get(0).getExternalKey(), transactionExternalKey);
+ assertEquals(payment.getTransactions().get(0).getPaymentId(), payment.getId());
+ assertEquals(payment.getTransactions().get(0).getAmount().compareTo(authAmount), 0);
+ assertEquals(payment.getTransactions().get(0).getCurrency(), Currency.AED);
+ assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(authAmount), 0);
+ assertEquals(payment.getTransactions().get(0).getProcessedCurrency(), Currency.AED);
+
+ assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payment.getTransactions().get(0).getTransactionType(), TransactionType.AUTHORIZE);
+ assertNotNull(payment.getTransactions().get(0).getGatewayErrorMsg());
+ assertNotNull(payment.getTransactions().get(0).getGatewayErrorCode());
+
+ final Payment payment2 = paymentApi.createCapture(account, payment.getId(), captureAmount, Currency.AED, transactionExternalKey2,
+ ImmutableList.<PluginProperty>of(), callContext);
+
+ assertEquals(payment2.getExternalKey(), paymentExternalKey);
+ assertEquals(payment2.getPaymentMethodId(), account.getPaymentMethodId());
+ assertEquals(payment2.getAccountId(), account.getId());
+ assertEquals(payment2.getAuthAmount().compareTo(authAmount), 0);
+ assertEquals(payment2.getCapturedAmount().compareTo(captureAmount), 0);
+ assertEquals(payment2.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment2.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment2.getCurrency(), Currency.AED);
+ assertFalse(payment2.isAuthVoided());
+
+ assertEquals(payment2.getTransactions().size(), 2);
+ assertEquals(payment2.getTransactions().get(1).getExternalKey(), transactionExternalKey2);
+ assertEquals(payment2.getTransactions().get(1).getPaymentId(), payment.getId());
+ assertEquals(payment2.getTransactions().get(1).getAmount().compareTo(captureAmount), 0);
+ assertEquals(payment2.getTransactions().get(1).getCurrency(), Currency.AED);
+ assertEquals(payment2.getTransactions().get(1).getProcessedAmount().compareTo(captureAmount), 0);
+ assertEquals(payment2.getTransactions().get(1).getProcessedCurrency(), Currency.AED);
+
+ assertEquals(payment2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payment2.getTransactions().get(1).getTransactionType(), TransactionType.CAPTURE);
+ assertNotNull(payment2.getTransactions().get(1).getGatewayErrorMsg());
+ assertNotNull(payment2.getTransactions().get(1).getGatewayErrorCode());
+
+ // Void the capture
+ final Payment payment3 = paymentApi.createVoid(account, payment.getId(), transactionExternalKey3, ImmutableList.<PluginProperty>of(), callContext);
+
+ assertEquals(payment3.getExternalKey(), paymentExternalKey);
+ assertEquals(payment3.getPaymentMethodId(), account.getPaymentMethodId());
+ assertEquals(payment3.getAccountId(), account.getId());
+ assertEquals(payment3.getAuthAmount().compareTo(authAmount), 0);
+ assertEquals(payment3.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment3.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment3.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment3.getCurrency(), Currency.AED);
+ assertFalse(payment3.isAuthVoided());
+
+ assertEquals(payment3.getTransactions().size(), 3);
+ assertEquals(payment3.getTransactions().get(2).getExternalKey(), transactionExternalKey3);
+ assertEquals(payment3.getTransactions().get(2).getPaymentId(), payment.getId());
+ assertNull(payment3.getTransactions().get(2).getAmount());
+ assertNull(payment3.getTransactions().get(2).getCurrency());
+ assertNull(payment3.getTransactions().get(2).getProcessedAmount());
+ assertNull(payment3.getTransactions().get(2).getProcessedCurrency());
+
+ assertEquals(payment3.getTransactions().get(2).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payment3.getTransactions().get(2).getTransactionType(), TransactionType.VOID);
+ assertNotNull(payment3.getTransactions().get(2).getGatewayErrorMsg());
+ assertNotNull(payment3.getTransactions().get(2).getGatewayErrorCode());
+
+ // Void the authorization
+ final Payment payment4 = paymentApi.createVoid(account, payment.getId(), transactionExternalKey4, ImmutableList.<PluginProperty>of(), callContext);
+
+ assertEquals(payment4.getExternalKey(), paymentExternalKey);
+ assertEquals(payment4.getPaymentMethodId(), account.getPaymentMethodId());
+ assertEquals(payment4.getAccountId(), account.getId());
+ assertEquals(payment4.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment4.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment4.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment4.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment4.getCurrency(), Currency.AED);
+ assertTrue(payment4.isAuthVoided());
+
+ assertEquals(payment4.getTransactions().size(), 4);
+ assertEquals(payment4.getTransactions().get(3).getExternalKey(), transactionExternalKey4);
+ assertEquals(payment4.getTransactions().get(3).getPaymentId(), payment.getId());
+ assertNull(payment4.getTransactions().get(3).getAmount());
+ assertNull(payment4.getTransactions().get(3).getCurrency());
+ assertNull(payment4.getTransactions().get(3).getProcessedAmount());
+ assertNull(payment4.getTransactions().get(3).getProcessedCurrency());
+
+ assertEquals(payment4.getTransactions().get(3).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payment4.getTransactions().get(3).getTransactionType(), TransactionType.VOID);
+ assertNotNull(payment4.getTransactions().get(3).getGatewayErrorMsg());
+ assertNotNull(payment4.getTransactions().get(3).getGatewayErrorCode());
}
@Test(groups = "slow")
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentProcessor.java b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentProcessor.java
index 19f10b9..f5f71c6 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentProcessor.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentProcessor.java
@@ -138,7 +138,7 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
final String voidKey = UUID.randomUUID().toString();
final Payment voidTransaction = paymentProcessor.createVoid(true, null, account, paymentId, voidKey,
SHOULD_LOCK_ACCOUNT, PLUGIN_PROPERTIES, callContext, internalCallContext);
- verifyPayment(voidTransaction, paymentExternalKey, TEN, ZERO, ZERO, 2);
+ verifyPayment(voidTransaction, paymentExternalKey, ZERO, ZERO, ZERO, 2);
verifyPaymentTransaction(voidTransaction.getTransactions().get(1), voidKey, TransactionType.VOID, null, paymentId);
paymentBusListener.verify(2, account.getId(), paymentId, null);
}
@@ -241,7 +241,7 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(event.getPaymentId(), paymentId);
Assert.assertEquals(event.getAccountId(), accountId);
if (amount == null) {
- Assert.assertEquals(event.getAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertNull(event.getAmount());
} else {
Assert.assertEquals(event.getAmount().compareTo(amount), 0);
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
index 05d300f..d6fb5f3 100644
--- a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
@@ -261,7 +261,7 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
@Override
public PaymentTransactionInfoPlugin voidPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final Iterable<PluginProperty> properties, final CallContext context)
throws PaymentPluginApiException {
- return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.VOID, BigDecimal.ZERO, null, properties);
+ return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.VOID, null, null, properties);
}
@Override
@@ -364,7 +364,7 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.REFUND, refundAmount, currency, properties);
}
- private PaymentTransactionInfoPlugin getPaymentTransactionInfoPluginResult(final UUID kbPaymentId, final UUID kbTransactionId, final TransactionType type, final BigDecimal amount, @Nullable final Currency currency, final Iterable<PluginProperty> pluginProperties) throws PaymentPluginApiException {
+ private PaymentTransactionInfoPlugin getPaymentTransactionInfoPluginResult(final UUID kbPaymentId, final UUID kbTransactionId, final TransactionType type, @Nullable final BigDecimal amount, @Nullable final Currency currency, final Iterable<PluginProperty> pluginProperties) throws PaymentPluginApiException {
if (makePluginWaitSomeMilliseconds.get() > 0) {
try {
Thread.sleep(makePluginWaitSomeMilliseconds.get());
@@ -404,7 +404,8 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
payments.put(kbPaymentId.toString(), info);
}
- final BigDecimal processedAmount = MoreObjects.firstNonNull(overrideNextProcessedAmount.getAndSet(null), amount);
+ final BigDecimal overrideNextProcessedAmount = this.overrideNextProcessedAmount.getAndSet(null);
+ final BigDecimal processedAmount = overrideNextProcessedAmount != null ? overrideNextProcessedAmount : amount;
Currency processedCurrency = overrideNextProcessedCurrency.getAndSet(null);
if (processedCurrency == null) {
processedCurrency = currency;
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestUsage.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestUsage.java
index 2e9dd6a..dfc49e3 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestUsage.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestUsage.java
@@ -19,9 +19,11 @@ package org.killbill.billing.jaxrs;
import java.util.UUID;
+import org.killbill.billing.ErrorCode;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.client.KillBillClientException;
import org.killbill.billing.client.model.Account;
import org.killbill.billing.client.model.Bundle;
import org.killbill.billing.client.model.RolledUpUsage;
@@ -115,4 +117,62 @@ public class TestUsage extends TestJaxrsBase {
Assert.assertEquals(retrievedUsage4.getRolledUpUnits().get(0).getUnitType(), "bullets");
Assert.assertEquals((long) retrievedUsage4.getRolledUpUnits().get(0).getAmount(), 5);
}
+
+ @Test(groups = "slow", description = "Test tracking ID already exists")
+ public void testRecordUsageTrackingIdExists() throws Exception {
+
+ final Account accountJson = createAccountWithDefaultPaymentMethod();
+
+ final Subscription base = new Subscription();
+ base.setAccountId(accountJson.getAccountId());
+ base.setProductName("Pistol");
+ base.setProductCategory(ProductCategory.BASE);
+ base.setBillingPeriod(BillingPeriod.MONTHLY);
+ base.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ final Subscription addOn = new Subscription();
+ addOn.setAccountId(accountJson.getAccountId());
+ addOn.setProductName("Bullets");
+ addOn.setProductCategory(ProductCategory.ADD_ON);
+ addOn.setBillingPeriod(BillingPeriod.NO_BILLING_PERIOD);
+ addOn.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ final Bundle bundle = killBillClient.createSubscriptionWithAddOns(ImmutableList.<Subscription>of(base, addOn),
+ null,
+ DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC,
+ createdBy,
+ reason,
+ comment);
+ final UUID addOnSubscriptionId = Iterables.<Subscription>find(bundle.getSubscriptions(),
+ new Predicate<Subscription>() {
+ @Override
+ public boolean apply(final Subscription input) {
+ return ProductCategory.ADD_ON.equals(input.getProductCategory());
+ }
+ }).getSubscriptionId();
+
+ final UsageRecord usageRecord1 = new UsageRecord();
+ usageRecord1.setAmount(10L);
+ usageRecord1.setRecordDate(clock.getUTCToday().minusDays(1));
+
+ final UnitUsageRecord unitUsageRecord = new UnitUsageRecord();
+ unitUsageRecord.setUnitType("bullets");
+ unitUsageRecord.setUsageRecords(ImmutableList.<UsageRecord>of(usageRecord1));
+
+ final SubscriptionUsageRecord usage = new SubscriptionUsageRecord();
+ usage.setSubscriptionId(addOnSubscriptionId);
+ usage.setTrackingId(UUID.randomUUID().toString());
+ usage.setUnitUsageRecords(ImmutableList.<UnitUsageRecord>of(unitUsageRecord));
+
+ killBillClient.createSubscriptionUsageRecord(usage, createdBy, reason, comment);
+
+ try {
+ killBillClient.createSubscriptionUsageRecord(usage, createdBy, reason, comment);
+ Assert.fail();
+ }
+ catch (final KillBillClientException e) {
+ Assert.assertEquals(e.getBillingException().getCode(), (Integer) ErrorCode.USAGE_RECORD_TRACKING_ID_ALREADY_EXISTS.getCode());
+ }
+
+ }
}
diff --git a/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultUsageUserApi.java b/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultUsageUserApi.java
index 45b0146..74baa77 100644
--- a/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultUsageUserApi.java
+++ b/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultUsageUserApi.java
@@ -25,6 +25,7 @@ import java.util.UUID;
import javax.inject.Inject;
import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
@@ -32,6 +33,7 @@ import org.killbill.billing.usage.api.RolledUpUnit;
import org.killbill.billing.usage.api.RolledUpUsage;
import org.killbill.billing.usage.api.SubscriptionUsageRecord;
import org.killbill.billing.usage.api.UnitUsageRecord;
+import org.killbill.billing.usage.api.UsageApiException;
import org.killbill.billing.usage.api.UsageRecord;
import org.killbill.billing.usage.api.UsageUserApi;
import org.killbill.billing.usage.dao.RolledUpUsageDao;
@@ -40,6 +42,8 @@ import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.TenantContext;
+import com.google.common.base.Strings;
+
public class DefaultUsageUserApi implements UsageUserApi {
private final RolledUpUsageDao rolledUpUsageDao;
@@ -53,13 +57,21 @@ public class DefaultUsageUserApi implements UsageUserApi {
}
@Override
- public void recordRolledUpUsage(final SubscriptionUsageRecord record, final CallContext callContext) {
+ public void recordRolledUpUsage(final SubscriptionUsageRecord record, final CallContext callContext) throws UsageApiException {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(record.getSubscriptionId(), ObjectType.SUBSCRIPTION, callContext);
- for (UnitUsageRecord unitUsageRecord : record.getUnitUsageRecord()) {
- for (UsageRecord usageRecord : unitUsageRecord.getDailyAmount()) {
- rolledUpUsageDao.record(record.getSubscriptionId(), unitUsageRecord.getUnitType(), usageRecord.getDate(), usageRecord.getAmount(), internalCallContext);
+
+ // check if we have (at least) one row with the supplied tracking id
+ if(!Strings.isNullOrEmpty(record.getTrackingId()) && recordsWithTrackingIdExist(record, internalCallContext)){
+ throw new UsageApiException(ErrorCode.USAGE_RECORD_TRACKING_ID_ALREADY_EXISTS, record.getTrackingId());
+ }
+
+ final List<RolledUpUsageModelDao> usages = new ArrayList<RolledUpUsageModelDao>();
+ for (final UnitUsageRecord unitUsageRecord : record.getUnitUsageRecord()) {
+ for (final UsageRecord usageRecord : unitUsageRecord.getDailyAmount()) {
+ usages.add(new RolledUpUsageModelDao(record.getSubscriptionId(), unitUsageRecord.getUnitType(), usageRecord.getDate(), usageRecord.getAmount(), record.getTrackingId()));
}
}
+ rolledUpUsageDao.record(usages, internalCallContext);
}
@Override
@@ -98,4 +110,8 @@ public class DefaultUsageUserApi implements UsageUserApi {
}
return result;
}
+
+ private boolean recordsWithTrackingIdExist(SubscriptionUsageRecord record, InternalCallContext context){
+ return rolledUpUsageDao.recordsWithTrackingIdExist(record.getSubscriptionId(), record.getTrackingId(), context);
+ }
}
diff --git a/usage/src/main/java/org/killbill/billing/usage/dao/DefaultRolledUpUsageDao.java b/usage/src/main/java/org/killbill/billing/usage/dao/DefaultRolledUpUsageDao.java
index 3ab2973..4d3b4c5 100644
--- a/usage/src/main/java/org/killbill/billing/usage/dao/DefaultRolledUpUsageDao.java
+++ b/usage/src/main/java/org/killbill/billing/usage/dao/DefaultRolledUpUsageDao.java
@@ -16,7 +16,6 @@
package org.killbill.billing.usage.dao;
-import java.math.BigDecimal;
import java.util.List;
import java.util.UUID;
@@ -37,9 +36,13 @@ public class DefaultRolledUpUsageDao implements RolledUpUsageDao {
}
@Override
- public void record(final UUID subscriptionId, final String unitType, final LocalDate date, final Long amount, final InternalCallContext context) {
- final RolledUpUsageModelDao rolledUpUsageModelDao = new RolledUpUsageModelDao(subscriptionId, unitType, date, amount);
- rolledUpUsageSqlDao.create(rolledUpUsageModelDao, context);
+ public void record(final Iterable<RolledUpUsageModelDao> usages, final InternalCallContext context){
+ rolledUpUsageSqlDao.create(usages, context);
+ }
+
+ @Override
+ public Boolean recordsWithTrackingIdExist(final UUID subscriptionId, final String trackingId, final InternalTenantContext context){
+ return rolledUpUsageSqlDao.recordsWithTrackingIdExist(subscriptionId, trackingId, context) != null ;
}
@Override
diff --git a/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageDao.java b/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageDao.java
index e458635..9f6be8e 100644
--- a/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageDao.java
+++ b/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageDao.java
@@ -25,13 +25,13 @@ import org.killbill.billing.callcontext.InternalTenantContext;
public interface RolledUpUsageDao {
- void record(UUID subscriptionId, String unitType, LocalDate date,
- Long amount, InternalCallContext context);
+ void record(Iterable<RolledUpUsageModelDao> usages, InternalCallContext context);
+
+ Boolean recordsWithTrackingIdExist(UUID subscriptionId, String trackingId, InternalTenantContext context);
List<RolledUpUsageModelDao> getUsageForSubscription(UUID subscriptionId, LocalDate startDate, LocalDate endDate, String unitType, InternalTenantContext context);
List<RolledUpUsageModelDao> getAllUsageForSubscription(UUID subscriptionId, LocalDate startDate, LocalDate endDate, InternalTenantContext context);
-
List<RolledUpUsageModelDao> getRawUsageForAccount(LocalDate startDate, LocalDate endDate, InternalTenantContext context);
}
diff --git a/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageModelDao.java b/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageModelDao.java
index 411765a..5cd4eb1 100644
--- a/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageModelDao.java
+++ b/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageModelDao.java
@@ -26,25 +26,35 @@ import org.killbill.billing.util.entity.Entity;
import org.killbill.billing.util.entity.dao.EntityModelDao;
import org.killbill.billing.util.entity.dao.EntityModelDaoBase;
+import com.google.common.base.Strings;
+
public class RolledUpUsageModelDao extends EntityModelDaoBase implements EntityModelDao<Entity> {
private UUID subscriptionId;
private String unitType;
private LocalDate recordDate;
private Long amount;
+ private String trackingId;
public RolledUpUsageModelDao() { /* For the DAO mapper */ }
- public RolledUpUsageModelDao(final UUID id, final DateTime createdDate, final DateTime updatedDate, final UUID subscriptionId, final String unitType, final LocalDate recordDate, final Long amount) {
+ public RolledUpUsageModelDao(final UUID id, final DateTime createdDate, final DateTime updatedDate, final UUID subscriptionId, final String unitType, final LocalDate recordDate, final Long amount, final String trackingId) {
super(id, createdDate, updatedDate);
this.subscriptionId = subscriptionId;
this.unitType = unitType;
this.recordDate = recordDate;
this.amount = amount;
+
+ if(Strings.isNullOrEmpty(trackingId)){
+ this.trackingId = UUIDs.randomUUID().toString();
+ }
+ else {
+ this.trackingId = trackingId;
+ }
}
- public RolledUpUsageModelDao(final UUID subscriptionId, final String unitType, final LocalDate recordDate, final Long amount) {
- this(UUIDs.randomUUID(), null, null, subscriptionId, unitType, recordDate, amount);
+ public RolledUpUsageModelDao(final UUID subscriptionId, final String unitType, final LocalDate recordDate, final Long amount, final String trackingId) {
+ this(UUIDs.randomUUID(), null, null, subscriptionId, unitType, recordDate, amount, trackingId);
}
public UUID getSubscriptionId() {
@@ -79,6 +89,14 @@ public class RolledUpUsageModelDao extends EntityModelDaoBase implements EntityM
this.amount = amount;
}
+ public String getTrackingId() {
+ return trackingId;
+ }
+
+ public void setTrackingId(final String trackingId) {
+ this.trackingId = trackingId;
+ }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
@@ -88,6 +106,7 @@ public class RolledUpUsageModelDao extends EntityModelDaoBase implements EntityM
sb.append(", unitType='").append(unitType).append('\'');
sb.append(", recordDate=").append(recordDate);
sb.append(", amount=").append(amount);
+ sb.append(", trackingId=").append(trackingId);
sb.append('}');
return sb.toString();
}
@@ -118,7 +137,9 @@ public class RolledUpUsageModelDao extends EntityModelDaoBase implements EntityM
if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
return false;
}
-
+ if (trackingId != null ? !trackingId.equals(that.trackingId) : that.trackingId != null) {
+ return false;
+ }
return true;
}
@@ -129,6 +150,7 @@ public class RolledUpUsageModelDao extends EntityModelDaoBase implements EntityM
result = 31 * result + (unitType != null ? unitType.hashCode() : 0);
result = 31 * result + (recordDate != null ? recordDate.hashCode() : 0);
result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (trackingId != null ? trackingId.hashCode() : 0);
return result;
}
diff --git a/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.java b/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.java
index a10a2c0..89fc3a2 100644
--- a/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.java
+++ b/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.java
@@ -30,31 +30,36 @@ import org.killbill.billing.util.entity.dao.EntitySqlDao;
import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlBatch;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
-import org.skife.jdbi.v2.sqlobject.SqlUpdate;
@EntitySqlDaoStringTemplate
public interface RolledUpUsageSqlDao extends EntitySqlDao<RolledUpUsageModelDao, Entity> {
- @SqlUpdate
- public void create(@BindBean RolledUpUsageModelDao rolledUpUsage,
- @InternalTenantContextBinder final InternalCallContext context);
+ @SqlBatch
+ void create(@BindBean Iterable<RolledUpUsageModelDao> usages,
+ @InternalTenantContextBinder final InternalCallContext context);
@SqlQuery
- public List<RolledUpUsageModelDao> getUsageForSubscription(@Bind("subscriptionId") final UUID subscriptionId,
- @Bind("startDate") final Date startDate,
- @Bind("endDate") final Date endDate,
- @Bind("unitType") final String unitType,
- @InternalTenantContextBinder final InternalTenantContext context);
+ Long recordsWithTrackingIdExist(@Bind("subscriptionId") final UUID subscriptionId,
+ @Bind("trackingId") final String trackingId,
+ @InternalTenantContextBinder final InternalTenantContext context);
@SqlQuery
- public List<RolledUpUsageModelDao> getAllUsageForSubscription(@Bind("subscriptionId") final UUID subscriptionId,
- @Bind("startDate") final Date startDate,
- @Bind("endDate") final Date endDate,
- @InternalTenantContextBinder final InternalTenantContext context);
+ List<RolledUpUsageModelDao> getUsageForSubscription(@Bind("subscriptionId") final UUID subscriptionId,
+ @Bind("startDate") final Date startDate,
+ @Bind("endDate") final Date endDate,
+ @Bind("unitType") final String unitType,
+ @InternalTenantContextBinder final InternalTenantContext context);
@SqlQuery
- public List<RolledUpUsageModelDao> getRawUsageForAccount(@Bind("startDate") final Date startDate,
- @Bind("endDate") final Date endDate,
- @InternalTenantContextBinder final InternalTenantContext context);
+ List<RolledUpUsageModelDao> getAllUsageForSubscription(@Bind("subscriptionId") final UUID subscriptionId,
+ @Bind("startDate") final Date startDate,
+ @Bind("endDate") final Date endDate,
+ @InternalTenantContextBinder final InternalTenantContext context);
+
+ @SqlQuery
+ List<RolledUpUsageModelDao> getRawUsageForAccount(@Bind("startDate") final Date startDate,
+ @Bind("endDate") final Date endDate,
+ @InternalTenantContextBinder final InternalTenantContext context);
}
diff --git a/usage/src/main/resources/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.sql.stg b/usage/src/main/resources/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.sql.stg
index 94fcb03..b1d739c 100644
--- a/usage/src/main/resources/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.sql.stg
+++ b/usage/src/main/resources/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.sql.stg
@@ -8,6 +8,7 @@ tableFields(prefix) ::= <<
, <prefix>unit_type
, <prefix>record_date
, <prefix>amount
+, <prefix>tracking_id
, <prefix>created_by
, <prefix>created_date
>>
@@ -17,10 +18,21 @@ tableValues() ::= <<
, :unitType
, :recordDate
, :amount
+, :trackingId
, :userName
, :createdDate
>>
+recordsWithTrackingIdExist() ::= <<
+select
+ 1
+from <tableName()>
+where subscription_id = :subscriptionId
+and tracking_id = :trackingId
+<AND_CHECK_TENANT()>
+limit 1
+;
+>>
getUsageForSubscription() ::= <<
select
diff --git a/usage/src/main/resources/org/killbill/billing/usage/ddl.sql b/usage/src/main/resources/org/killbill/billing/usage/ddl.sql
index 44837a9..7c2418e 100644
--- a/usage/src/main/resources/org/killbill/billing/usage/ddl.sql
+++ b/usage/src/main/resources/org/killbill/billing/usage/ddl.sql
@@ -8,6 +8,7 @@ CREATE TABLE rolled_up_usage (
unit_type varchar(255),
record_date date NOT NULL,
amount bigint NOT NULL,
+ tracking_id varchar(128) NOT NULL,
created_by varchar(50) NOT NULL,
created_date datetime NOT NULL,
account_record_id bigint /*! unsigned */ not null,
@@ -18,3 +19,4 @@ CREATE UNIQUE INDEX rolled_up_usage_id ON rolled_up_usage(id);
CREATE INDEX rolled_up_usage_subscription_id ON rolled_up_usage(subscription_id ASC);
CREATE INDEX rolled_up_usage_tenant_account_record_id ON rolled_up_usage(tenant_record_id, account_record_id);
CREATE INDEX rolled_up_usage_account_record_id ON rolled_up_usage(account_record_id);
+CREATE INDEX rolled_up_usage_tracking_id_subscription_id_tenant_record_id ON rolled_up_usage(tracking_id, subscription_id, tenant_record_id);
diff --git a/usage/src/test/java/org/killbill/billing/usage/dao/TestDefaultRolledUpUsageDao.java b/usage/src/test/java/org/killbill/billing/usage/dao/TestDefaultRolledUpUsageDao.java
index 5be6e0b..69a6cde 100644
--- a/usage/src/test/java/org/killbill/billing/usage/dao/TestDefaultRolledUpUsageDao.java
+++ b/usage/src/test/java/org/killbill/billing/usage/dao/TestDefaultRolledUpUsageDao.java
@@ -17,14 +17,18 @@
package org.killbill.billing.usage.dao;
+import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.joda.time.LocalDate;
import org.killbill.billing.usage.UsageTestSuiteWithEmbeddedDB;
+import org.killbill.billing.util.UUIDs;
+import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
public class TestDefaultRolledUpUsageDao extends UsageTestSuiteWithEmbeddedDB {
@@ -37,8 +41,12 @@ public class TestDefaultRolledUpUsageDao extends UsageTestSuiteWithEmbeddedDB {
final Long amount1 = 10L;
final Long amount2 = 5L;
- rolledUpUsageDao.record(subscriptionId, unitType, startDate, amount1, internalCallContext);
- rolledUpUsageDao.record(subscriptionId, unitType, endDate.minusDays(1), amount2, internalCallContext);
+ RolledUpUsageModelDao usage1 = new RolledUpUsageModelDao(subscriptionId, unitType, startDate, amount1, UUID.randomUUID().toString());
+ RolledUpUsageModelDao usage2 = new RolledUpUsageModelDao(subscriptionId, unitType, endDate.minusDays(1), amount2, UUID.randomUUID().toString());
+ List<RolledUpUsageModelDao> usages = new ArrayList<RolledUpUsageModelDao>();
+ usages.add(usage1);
+ usages.add(usage2);
+ rolledUpUsageDao.record(usages, internalCallContext);
final List<RolledUpUsageModelDao> result = rolledUpUsageDao.getUsageForSubscription(subscriptionId, startDate, endDate, unitType, internalCallContext);
assertEquals(result.size(), 2);
@@ -63,10 +71,14 @@ public class TestDefaultRolledUpUsageDao extends UsageTestSuiteWithEmbeddedDB {
final Long amount2 = 5L;
final Long amount3 = 13L;
- rolledUpUsageDao.record(subscriptionId, unitType1, startDate, amount1, internalCallContext);
- rolledUpUsageDao.record(subscriptionId, unitType1, startDate.plusDays(1), amount2, internalCallContext);
-
- rolledUpUsageDao.record(subscriptionId, unitType2, endDate.minusDays(1), amount3, internalCallContext);
+ RolledUpUsageModelDao usage1 = new RolledUpUsageModelDao(subscriptionId, unitType1, startDate, amount1, UUID.randomUUID().toString());
+ RolledUpUsageModelDao usage2 = new RolledUpUsageModelDao(subscriptionId, unitType1, startDate.plusDays(1), amount2, UUID.randomUUID().toString());
+ RolledUpUsageModelDao usage3 = new RolledUpUsageModelDao(subscriptionId, unitType2, endDate.minusDays(1), amount3, UUID.randomUUID().toString());
+ List<RolledUpUsageModelDao> usages = new ArrayList<RolledUpUsageModelDao>();
+ usages.add(usage1);
+ usages.add(usage2);
+ usages.add(usage3);
+ rolledUpUsageDao.record(usages, internalCallContext);
final List<RolledUpUsageModelDao> result = rolledUpUsageDao.getAllUsageForSubscription(subscriptionId, startDate, endDate, internalCallContext);
assertEquals(result.size(), 3);
@@ -91,9 +103,71 @@ public class TestDefaultRolledUpUsageDao extends UsageTestSuiteWithEmbeddedDB {
final LocalDate startDate = new LocalDate(2013, 1, 1);
final LocalDate endDate = new LocalDate(2013, 2, 1);
- rolledUpUsageDao.record(subscriptionId, unitType, endDate, 9L, internalCallContext);
+ RolledUpUsageModelDao usage1 = new RolledUpUsageModelDao(subscriptionId, unitType, endDate, 9L, UUID.randomUUID().toString());
+ List<RolledUpUsageModelDao> usages = new ArrayList<RolledUpUsageModelDao>();
+ usages.add(usage1);
+ rolledUpUsageDao.record(usages, internalCallContext);
final List<RolledUpUsageModelDao> result = rolledUpUsageDao.getUsageForSubscription(subscriptionId, startDate, endDate, unitType, internalCallContext);
assertEquals(result.size(), 0);
}
-}
+
+ @Test(groups = "slow")
+ public void testDuplicateRecords() {
+ final UUID subscriptionId = UUID.randomUUID();
+ final String unitType1 = "foo";
+ final String unitType2 = "bar";
+ final LocalDate startDate = new LocalDate(2013, 1, 1);
+ final LocalDate endDate = new LocalDate(2013, 2, 1);
+ final Long amount1 = 10L;
+ final Long amount2 = 5L;
+ final Long amount3 = 13L;
+
+ RolledUpUsageModelDao usage1 = new RolledUpUsageModelDao(subscriptionId, unitType1, startDate, amount1, UUID.randomUUID().toString());
+ RolledUpUsageModelDao usage2 = new RolledUpUsageModelDao(subscriptionId, unitType1, startDate.plusDays(1), amount2, UUID.randomUUID().toString());
+ RolledUpUsageModelDao usage3 = new RolledUpUsageModelDao(subscriptionId, unitType2, endDate.minusDays(1), amount3, UUID.randomUUID().toString());
+
+ List<RolledUpUsageModelDao> usages = new ArrayList<RolledUpUsageModelDao>();
+ usages.add(usage1);
+ usages.add(usage2);
+ usages.add(usage3);
+ rolledUpUsageDao.record(usages, internalCallContext);
+
+ final List<RolledUpUsageModelDao> result = rolledUpUsageDao.getAllUsageForSubscription(subscriptionId, startDate, endDate, internalCallContext);
+ assertEquals(result.size(), 3);
+
+ try {
+ rolledUpUsageDao.record(usages, internalCallContext);
+ fail("duplicate records accepted");
+ } catch (UnableToExecuteStatementException e) {
+ assertEquals(result.size(), 3);
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testRecordsWithTrackingIdExist() {
+ final UUID subscriptionId = UUIDs.randomUUID();
+ final String unitType1 = "foo";
+ final String unitType2 = "bar";
+ final LocalDate startDate = new LocalDate(2013, 1, 1);
+ final LocalDate endDate = new LocalDate(2013, 2, 1);
+ final Long amount1 = 10L;
+ final Long amount2 = 5L;
+ final Long amount3 = 13L;
+
+ String trackingId = UUIDs.randomUUID().toString();
+
+ RolledUpUsageModelDao usage1 = new RolledUpUsageModelDao(subscriptionId, unitType1, startDate, amount1, trackingId);
+ RolledUpUsageModelDao usage2 = new RolledUpUsageModelDao(subscriptionId, unitType1, startDate.plusDays(1), amount2, trackingId);
+ RolledUpUsageModelDao usage3 = new RolledUpUsageModelDao(subscriptionId, unitType2, endDate.minusDays(1), amount3, UUID.randomUUID().toString());
+
+ List<RolledUpUsageModelDao> usages = new ArrayList<RolledUpUsageModelDao>();
+ usages.add(usage1);
+ usages.add(usage2);
+ usages.add(usage3);
+ rolledUpUsageDao.record(usages, internalCallContext);
+
+ assertEquals(rolledUpUsageDao.recordsWithTrackingIdExist(subscriptionId, trackingId, internalCallContext),
+ Boolean.TRUE);
+ }
+}
\ No newline at end of file
diff --git a/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
index 13022fe..d05e54e 100644
--- a/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
+++ b/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
@@ -298,7 +298,6 @@ values (
<accountRecordIdValueWithComma()>
<tenantRecordIdValueWithComma()>
)
-;
>>
/** Audits, History **/