killbill-aplcache

Merge branch 'work-for-release-0.17.0' of github.com:killbill/killbill

3/20/2016 6:38:07 PM

Changes

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 **/