killbill-uncached

Changes

api/src/main/java/org/killbill/billing/events/RepairSubscriptionInternalEvent.java 30(+0 -30)

bin/db-helper 28(+26 -2)

pom.xml 2(+1 -1)

util/pom.xml 30(+30 -0)

util/src/main/java/org/killbill/billing/util/callcontext/MigrationCallContext.java 43(+0 -43)

Details

diff --git a/account/src/main/java/org/killbill/billing/account/dao/DefaultAccountDao.java b/account/src/main/java/org/killbill/billing/account/dao/DefaultAccountDao.java
index 19eb29a..7d9815d 100644
--- a/account/src/main/java/org/killbill/billing/account/dao/DefaultAccountDao.java
+++ b/account/src/main/java/org/killbill/billing/account/dao/DefaultAccountDao.java
@@ -94,7 +94,7 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
         try {
             eventBus.postFromTransaction(creationEvent, entitySqlDaoWrapperFactory.getHandle().getConnection());
         } catch (final EventBusException e) {
-            log.warn("Failed to post account creation event for account " + savedAccount.getId(), e);
+            log.warn("Failed to post account creation event for accountId='{}'", savedAccount.getId(), e);
         }
     }
 
@@ -166,7 +166,7 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
                 try {
                     eventBus.postFromTransaction(changeEvent, entitySqlDaoWrapperFactory.getHandle().getConnection());
                 } catch (final EventBusException e) {
-                    log.warn("Failed to post account change event for account " + accountId, e);
+                    log.warn("Failed to post account change event for accountId='{}'", accountId, e);
                 }
 
                 return null;
@@ -205,7 +205,7 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
                 try {
                     eventBus.postFromTransaction(changeEvent, entitySqlDaoWrapperFactory.getHandle().getConnection());
                 } catch (final EventBusException e) {
-                    log.warn("Failed to post account change event for account " + accountId, e);
+                    log.warn("Failed to post account change event for accountId='{}'", accountId, e);
                 }
                 return null;
             }
diff --git a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
index 6c44797..5f9d7b5 100644
--- a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
@@ -37,9 +37,9 @@ public interface InvoiceInternalApi {
 
     public BigDecimal getAccountBalance(UUID accountId, InternalTenantContext context);
 
-    public void notifyOfPayment(UUID invoiceId, BigDecimal amountOutstanding, Currency currency, Currency processedCurrency, UUID paymentId, DateTime paymentDate, boolean success, InternalCallContext context) throws InvoiceApiException;
+    public void recordPaymentAttemptInit(UUID invoiceId, BigDecimal amountOutstanding, Currency currency, Currency processedCurrency, UUID paymentId, String transactionExternalKey, DateTime paymentDate, InternalCallContext context) throws InvoiceApiException;
 
-    public void notifyOfPayment(InvoicePayment invoicePayment, InternalCallContext context) throws InvoiceApiException;
+    public void recordPaymentAttemptCompletion(UUID invoiceId, BigDecimal amountOutstanding, Currency currency, Currency processedCurrency, UUID paymentId, String transactionExternalKey, DateTime paymentDate, boolean success, InternalCallContext context) throws InvoiceApiException;
 
     public InvoicePayment getInvoicePaymentForAttempt(UUID paymentId, InternalTenantContext context) throws InvoiceApiException;
 
@@ -59,10 +59,10 @@ public interface InvoiceInternalApi {
      * @return the created invoice payment object associated with this refund
      * @throws InvoiceApiException
      */
-    public InvoicePayment createRefund(UUID paymentId, BigDecimal amount, boolean isInvoiceAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts,
+    public InvoicePayment recordRefund(UUID paymentId, BigDecimal amount, boolean isInvoiceAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts,
                                        String transactionExternalKey, InternalCallContext context) throws InvoiceApiException;
 
-    public InvoicePayment createChargeback(UUID paymentId, BigDecimal amount, Currency currency, InternalCallContext context) throws InvoiceApiException;
+    public InvoicePayment recordChargeback(UUID paymentId, BigDecimal amount, Currency currency, InternalCallContext context) throws InvoiceApiException;
 
     /**
      * Rebalance CBA for account which have credit and unpaid invoices
diff --git a/beatrix/src/main/java/org/killbill/billing/beatrix/extbus/BeatrixListener.java b/beatrix/src/main/java/org/killbill/billing/beatrix/extbus/BeatrixListener.java
index 31389da..98a8df8 100644
--- a/beatrix/src/main/java/org/killbill/billing/beatrix/extbus/BeatrixListener.java
+++ b/beatrix/src/main/java/org/killbill/billing/beatrix/extbus/BeatrixListener.java
@@ -106,10 +106,10 @@ public class BeatrixListener {
             // However when using InMemoryBus, this can happen as there is no retry logic (at the 'ext' bus level) and so we should re-throw at this level to kick-in the retry logic from the 'main' bus
             // (The use of RuntimeException is somewhat arbitrary)
             //
-            log.warn("Failed to dispatch external bus events", e);
+            log.warn("Failed to post event {}", event, e);
             throw new RuntimeException(e);
         } catch (JsonProcessingException e) {
-            log.warn("Failed to dispatch external bus events", e);
+            log.warn("Failed to post event {}", event, e);
         }
     }
 
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
index e79eb43..2f714dd 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
@@ -18,6 +18,7 @@
 package org.killbill.billing.beatrix.integration;
 
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -25,6 +26,7 @@ import java.util.UUID;
 
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountData;
@@ -41,8 +43,14 @@ import org.killbill.billing.invoice.api.InvoiceItemType;
 import org.killbill.billing.invoice.api.InvoicePaymentType;
 import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
 import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentTransaction;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.payment.api.TransactionStatus;
+import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.skife.jdbi.v2.tweak.VoidHandleCallback;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -388,7 +396,7 @@ public class TestInvoicePayment extends TestIntegrationBase {
         // Verify links for payment 1
         Assert.assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(new BigDecimal("4.00")), 0);
         Assert.assertNull(invoice1.getPayments().get(0).getLinkedInvoicePaymentId());
-        Assert.assertNull(invoice1.getPayments().get(0).getPaymentCookieId());
+        Assert.assertEquals(invoice1.getPayments().get(0).getPaymentCookieId(), payment1.getTransactions().get(0).getExternalKey());
         Assert.assertEquals(invoice1.getPayments().get(0).getPaymentId(), payment1.getId());
         Assert.assertEquals(invoice1.getPayments().get(0).getType(), InvoicePaymentType.ATTEMPT);
         Assert.assertTrue(invoice1.getPayments().get(0).isSuccess());
@@ -396,7 +404,7 @@ public class TestInvoicePayment extends TestIntegrationBase {
         // Verify links for payment 2
         Assert.assertEquals(invoice1.getPayments().get(1).getAmount().compareTo(new BigDecimal("6.00")), 0);
         Assert.assertNull(invoice1.getPayments().get(1).getLinkedInvoicePaymentId());
-        Assert.assertNull(invoice1.getPayments().get(1).getPaymentCookieId());
+        Assert.assertEquals(invoice1.getPayments().get(1).getPaymentCookieId(), payment2.getTransactions().get(0).getExternalKey());
         Assert.assertEquals(invoice1.getPayments().get(1).getPaymentId(), payment2.getId());
         Assert.assertEquals(invoice1.getPayments().get(1).getType(), InvoicePaymentType.ATTEMPT);
         Assert.assertTrue(invoice1.getPayments().get(1).isSuccess());
@@ -482,4 +490,79 @@ public class TestInvoicePayment extends TestIntegrationBase {
         assertEquals(payments2.get(0).getTransactions().get(1).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
         assertEquals(payments2.get(0).getTransactions().get(1).getProcessedCurrency(), Currency.USD);
     }
+
+    @Test(groups = "slow")
+    public void testWithIncompletePaymentAttempt() throws Exception {
+        // 2012-05-01T00:03:42.000Z
+        clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+        final AccountData accountData = getAccountData(0);
+        final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+        accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+        final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+        invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 5, 1), callContext);
+
+        // 2012-05-31 => DAY 30 have to get out of trial {I0, P0}
+        addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+        Invoice invoice2 = invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 30), callContext);
+
+        // Invoice is fully paid
+        final Payment originalPayment = paymentChecker.checkPayment(account.getId(), 1, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 5, 31), new BigDecimal("249.95"), TransactionStatus.SUCCESS, invoice2.getId(), Currency.USD));
+        Assert.assertEquals(originalPayment.getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+        Assert.assertEquals(originalPayment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+        Assert.assertEquals(originalPayment.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+        Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.ZERO), 0);
+        Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+
+        final PaymentTransaction originalTransaction = originalPayment.getTransactions().get(0);
+
+        // Let 's hack invoice_payment table by hand to simulate a non completion of the payment (onSuccessCall was never called)
+        dbi.withHandle(new HandleCallback<Void>() {
+            @Override
+            public Void withHandle(final Handle handle) throws Exception {
+                handle.execute("update invoice_payments set success = false where payment_cookie_id = ?", originalTransaction.getExternalKey());
+                return null;
+            }
+        });
+
+        final Invoice updateInvoice2 = invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        // Invoice now shows as unpaid
+        Assert.assertEquals(updateInvoice2.getBalance().compareTo(originalPayment.getPurchasedAmount()), 0);
+        Assert.assertEquals(updateInvoice2.getPayments().size(), 1);
+        Assert.assertEquals(updateInvoice2.getPayments().get(0).getPaymentCookieId(), originalTransaction.getExternalKey());
+
+        //
+        // Now trigger invoice payment again (no new payment should be made as code should detect broken state and fix it by itself)
+        // We expect an INVOICE_PAYMENT that indicates the invoice was repaired, and also an exception because plugin aborts payment call since there is nothing to do.
+        //
+        busHandler.pushExpectedEvents(NextEvent.INVOICE_PAYMENT);
+        final List<PluginProperty> properties = new ArrayList<PluginProperty>();
+        final PluginProperty prop1 = new PluginProperty(InvoicePaymentControlPluginApi.PROP_IPCD_INVOICE_ID, updateInvoice2.getId().toString(), false);
+        properties.add(prop1);
+        try {
+            paymentApi.createPurchaseWithPaymentControl(account, account.getPaymentMethodId(), null, updateInvoice2.getBalance(), updateInvoice2.getCurrency(), UUID.randomUUID().toString(),
+                                                        UUID.randomUUID().toString(), properties, PAYMENT_OPTIONS, refreshedCallContext());
+            Assert.fail("The payment should not succeed (and yet it will repair the broken state....)");
+        } catch (final PaymentApiException expected) {
+            Assert.assertEquals(expected.getCode(), ErrorCode.PAYMENT_PLUGIN_EXCEPTION.getCode());
+        }
+        assertListenerStatus();
+
+        final Invoice updateInvoice3 = invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        Assert.assertEquals(updateInvoice3.getBalance().compareTo(BigDecimal.ZERO), 0);
+        Assert.assertEquals(updateInvoice3.getPayments().size(), 1);
+        Assert.assertEquals(updateInvoice3.getPayments().get(0).getPaymentCookieId(), originalTransaction.getExternalKey());
+        Assert.assertTrue(updateInvoice3.getPayments().get(0).isSuccess());
+        Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+        final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, ImmutableList.<PluginProperty>of(), callContext);
+        Assert.assertEquals(payments.size(), 1);
+        Assert.assertEquals(payments.get(0).getTransactions().size(), 1);
+
+    }
 }

bin/db-helper 28(+26 -2)

diff --git a/bin/db-helper b/bin/db-helper
index c642b1c..7b9c051 100755
--- a/bin/db-helper
+++ b/bin/db-helper
@@ -60,7 +60,7 @@ eval set -- "${ARGS}"
 
 function usage() {
     echo -n "./db_helper"
-    echo -n " -a|--action <create|clean|dump>"
+    echo -n " -a|--action <create|clean|dump|migrate|dryRunMigrate|repair|info>"
     echo -n " --driver <mysql|postgres> (default = mysql)"
     echo -n " -h|--host host (default = localhost)"
     echo -n " --port port"
@@ -159,6 +159,20 @@ function cleanup() {
     rm -f "/tmp/*.$$"
 }
 
+function flyway() {
+    flyway_bin=util/target/killbill-flyway.jar
+    if [ ! -f "$flyway_bin" ]; then
+        echo "File $flyway_bin does not exists - build util first"
+        usage
+    fi
+
+    locations=
+    for migration_dir in `find */src/main/resources -type d -name migration`; do
+        locations="${locations}filesystem:$migration_dir,"
+    done
+
+    java -jar $flyway_bin -locations=$locations -user=$USER -password=$PWD -url=$URL ${@}
+}
 
 while true; do
   case "$1" in
@@ -178,7 +192,7 @@ done
 
 
 if [ -z $ACTION ]; then
-    echo "Need to specify an action <CREATE|CLEAN|DUMP>"
+    echo "Need to specify an action"
     usage
 fi
 
@@ -195,6 +209,12 @@ fi
 if [ $DRIVER == "postgres" ] && [ -z $PORT ]; then
     PORT=$PORT_POSTGRES
 fi
+if [ $DRIVER == "mysql" ] && [ -z $URL ]; then
+    URL=jdbc:mysql://$HOST:$PORT/$DATABASE
+fi
+if [ $DRIVER == "postgres" ] && [ -z $URL ]; then
+    URL=jdbc:postgresql://$HOST:$PORT/$DATABASE
+fi
 
 
 if [ $ACTION == "dump" ]; then
@@ -233,4 +253,8 @@ if [ $ACTION == "clean" ]; then
     fi
 fi
 
+if [ $ACTION == "migrate" ] || [ $ACTION == "dryRunMigrate" ] || [ $ACTION == "repair" ] || [ $ACTION == "info" ]; then
+    flyway $ACTION
+fi
+
 cleanup
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/caching/CatalogCacheInvalidationCallback.java b/catalog/src/main/java/org/killbill/billing/catalog/caching/CatalogCacheInvalidationCallback.java
index 63548dc..852cd06 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/caching/CatalogCacheInvalidationCallback.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/caching/CatalogCacheInvalidationCallback.java
@@ -38,7 +38,7 @@ public class CatalogCacheInvalidationCallback implements CacheInvalidationCallba
 
     @Override
     public void invalidateCache(TenantKey key, final Object cookie, final InternalTenantContext tenantContext) {
-        log.info("Invalidate catalog cache for tenant {} ", tenantContext.getTenantRecordId());
+        log.info("Invalidate catalog cache for tenantRecordId='{}'", tenantContext.getTenantRecordId());
         catalogCache.clearCatalog(tenantContext);
     }
 }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheCatalogCache.java b/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheCatalogCache.java
index 44e704d..5713049 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheCatalogCache.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheCatalogCache.java
@@ -155,7 +155,7 @@ public class EhCacheCatalogCache implements CatalogCache {
             this.defaultCatalog = loader.loadDefaultCatalog("EmptyCatalog.xml");
         } catch (final CatalogApiException e) {
             this.defaultCatalog = new VersionedCatalog();
-            logger.warn("Exception loading EmptyCatalog - should never happen!", e);
+            logger.error("Exception loading EmptyCatalog - should never happen!", e);
         }
     }
 }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultCatalogService.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultCatalogService.java
index 053122c..cf17853 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultCatalogService.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultCatalogService.java
@@ -72,7 +72,7 @@ public class DefaultCatalogService implements KillbillService, CatalogService {
                 // In multi-tenant mode, the property is not required
                 if (config.getCatalogURI() != null && !config.getCatalogURI().isEmpty()) {
                     catalogCache.loadDefaultCatalog(config.getCatalogURI());
-                    log.info("Successfully loaded the default catalog " + config.getCatalogURI());
+                    log.info("Successfully loaded the default catalog {}", config.getCatalogURI());
                 }
                 isInitialized = true;
             } catch (Exception e) {
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/provider/DefaultCatalogProviderPluginRegistry.java b/catalog/src/main/java/org/killbill/billing/catalog/provider/DefaultCatalogProviderPluginRegistry.java
index e7e4005..d29ce36 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/provider/DefaultCatalogProviderPluginRegistry.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/provider/DefaultCatalogProviderPluginRegistry.java
@@ -41,13 +41,13 @@ public class DefaultCatalogProviderPluginRegistry implements OSGIServiceRegistra
 
     @Override
     public void registerService(final OSGIServiceDescriptor desc, final CatalogPluginApi service) {
-        log.info("DefaultInvoiceProviderPluginRegistry registering service " + desc.getRegistrationName());
+        log.info("Registering service='{}'", desc.getRegistrationName());
         pluginsByName.put(desc.getRegistrationName(), service);
     }
 
     @Override
     public void unregisterService(final String serviceName) {
-        log.info("DefaultInvoiceProviderPluginRegistry unregistering service " + serviceName);
+        log.info("Unregistering service='{}'", serviceName);
         pluginsByName.remove(serviceName);
     }
 
diff --git a/currency/src/main/java/org/killbill/billing/currency/DefaultCurrencyProviderPluginRegistry.java b/currency/src/main/java/org/killbill/billing/currency/DefaultCurrencyProviderPluginRegistry.java
index abb6abd..310eb05 100644
--- a/currency/src/main/java/org/killbill/billing/currency/DefaultCurrencyProviderPluginRegistry.java
+++ b/currency/src/main/java/org/killbill/billing/currency/DefaultCurrencyProviderPluginRegistry.java
@@ -42,13 +42,13 @@ public class DefaultCurrencyProviderPluginRegistry implements OSGIServiceRegistr
 
     @Override
     public void registerService(final OSGIServiceDescriptor desc, final CurrencyPluginApi service) {
-        log.info("DefaultCurrencyProviderPluginRegistry registering service " + desc.getRegistrationName());
+        log.info("Registering service='{}'", desc.getRegistrationName());
         pluginsByName.put(desc.getRegistrationName(), service);
     }
 
     @Override
     public void unregisterService(final String serviceName) {
-        log.info("DefaultCurrencyProviderPluginRegistry unregistering service " + serviceName);
+        log.info("Unregistering service='{}'", serviceName);
         pluginsByName.remove(serviceName);
     }
 
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionApi.java
index e74f7ef..959435f 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionApi.java
@@ -204,7 +204,7 @@ public class DefaultSubscriptionApi implements SubscriptionApi {
                                                       try {
                                                           return getSubscriptionBundle(subscriptionBaseBundle.getId(), context);
                                                       } catch (final SubscriptionApiException e) {
-                                                          log.warn("Error retrieving subscription", e);
+                                                          log.warn("Error retrieving bundleId='{}'", subscriptionBaseBundle.getId(), e);
                                                           return null;
                                                       }
                                                   }
@@ -228,7 +228,7 @@ public class DefaultSubscriptionApi implements SubscriptionApi {
                                                       try {
                                                           return getSubscriptionBundle(subscriptionBaseBundle.getId(), context);
                                                       } catch (final SubscriptionApiException e) {
-                                                          log.warn("Error retrieving subscription", e);
+                                                          log.warn("Error retrieving bundleId='{}'", subscriptionBaseBundle.getId(), e);
                                                           return null;
                                                       }
                                                   }
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java
index 1198f44..7d1ef3d 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java
@@ -189,7 +189,7 @@ public class DefaultEntitlementApiBase {
                     try {
                         eventBus.post(event);
                     } catch (EventBusException e) {
-                        log.warn("Failed to post bus event for pause operation on bundle " + bundleId);
+                        log.warn("Failed to post event {}", event, e);
                     }
 
                 } catch (SubscriptionBaseApiException e) {
@@ -241,7 +241,7 @@ public class DefaultEntitlementApiBase {
                     try {
                         eventBus.post(event);
                     } catch (EventBusException e) {
-                        log.warn("Failed to post bus event for resume operation on bundle " + bundleId);
+                        log.warn("Failed to post event {}", event, e);
                     }
 
                 } catch (SubscriptionBaseApiException e) {
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/dao/DefaultBlockingStateDao.java b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/DefaultBlockingStateDao.java
index cb1551e..7615862 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/dao/DefaultBlockingStateDao.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/DefaultBlockingStateDao.java
@@ -334,7 +334,7 @@ public class DefaultBlockingStateDao extends EntityDaoBase<BlockingStateModelDao
         try {
             eventBus.postFromTransaction(event, entitySqlDaoWrapperFactory.getHandle().getConnection());
         } catch (final EventBusException e) {
-            log.warn("Failed to post event {}", e);
+            log.warn("Failed to post event {}", event, e);
         }
     }
 
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/DefaultEntitlementService.java b/entitlement/src/main/java/org/killbill/billing/entitlement/DefaultEntitlementService.java
index 8369a8a..d74b8be 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/DefaultEntitlementService.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/DefaultEntitlementService.java
@@ -107,7 +107,7 @@ public class DefaultEntitlementService implements EntitlementService {
                     } else if (inputKey instanceof BlockingTransitionNotificationKey) {
                         processBlockingNotification((BlockingTransitionNotificationKey) inputKey, internalCallContext);
                     } else if (inputKey != null) {
-                        log.error("Entitlement service received an unexpected event type {}" + inputKey.getClass());
+                        log.error("Entitlement service received an unexpected event className='{}", inputKey.getClass());
                     } else {
                         log.error("Entitlement service received an unexpected null event");
                     }
@@ -127,12 +127,12 @@ public class DefaultEntitlementService implements EntitlementService {
         try {
             entitlement = entitlementInternalApi.getEntitlementForId(key.getEntitlementId(), internalCallContext);
         } catch (final EntitlementApiException e) {
-            log.error("Error retrieving entitlement for id " + key.getEntitlementId(), e);
+            log.error("Error retrieving entitlementId='{}'", key.getEntitlementId(), e);
             return;
         }
 
         if (!(entitlement instanceof DefaultEntitlement)) {
-            log.error("Entitlement service received an unexpected entitlement class type {}" + entitlement.getClass().getName());
+            log.error("Error retrieving entitlementId='{}', unexpected entitlement className='{}'", key.getEntitlementId(), entitlement.getClass().getName());
             return;
         }
 
@@ -147,7 +147,7 @@ public class DefaultEntitlementService implements EntitlementService {
                 entitlementInternalApi.resume(key.getBundleId(), internalCallContext.toLocalDate(key.getEffectiveDate(), ((DefaultEntitlement) entitlement).getSubscriptionBase().getStartDate()), ImmutableList.<PluginProperty>of(), internalCallContext);
             }
         } catch (final EntitlementApiException e) {
-            log.error("Error processing event for entitlement {}" + entitlement.getId(), e);
+            log.error("Error processing event for entitlementId='{}'", entitlement.getId(), e);
         }
     }
 
@@ -190,7 +190,7 @@ public class DefaultEntitlementService implements EntitlementService {
         try {
             eventBus.post(event);
         } catch (final EventBusException e) {
-            log.warn("Failed to post event {}", e);
+            log.warn("Failed to post event {}", event, e);
         }
     }
 
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/provider/DefaultEntitlementProviderPluginRegistry.java b/entitlement/src/main/java/org/killbill/billing/entitlement/provider/DefaultEntitlementProviderPluginRegistry.java
index 54b1f19..9f0b11a 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/provider/DefaultEntitlementProviderPluginRegistry.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/provider/DefaultEntitlementProviderPluginRegistry.java
@@ -41,13 +41,13 @@ public class DefaultEntitlementProviderPluginRegistry implements OSGIServiceRegi
 
     @Override
     public void registerService(final OSGIServiceDescriptor desc, final EntitlementPluginApi service) {
-        log.info("DefaultEntitlementProviderPluginRegistry registering service " + desc.getRegistrationName());
+        log.info("Registering service='{}'", desc.getRegistrationName());
         pluginsByName.put(desc.getRegistrationName(), service);
     }
 
     @Override
     public void unregisterService(final String serviceName) {
-        log.info("DefaultEntitlementProviderPluginRegistry unregistering service " + serviceName);
+        log.info("Unregistering service='{}'", serviceName);
         pluginsByName.remove(serviceName);
     }
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceService.java b/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceService.java
index 961d3e7..1c20ed9 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceService.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceService.java
@@ -56,7 +56,7 @@ public class DefaultInvoiceService implements InvoiceService {
             eventBus.register(invoiceListener);
             eventBus.register(tagHandler);
         } catch (PersistentBus.EventBusException e) {
-            throw new RuntimeException("Unable to register to the EventBus!", e);
+            throw new RuntimeException("Failed to register bus handlers", e);
         }
         dateNotifier.initialize();
     }
@@ -72,7 +72,7 @@ public class DefaultInvoiceService implements InvoiceService {
             eventBus.unregister(invoiceListener);
             eventBus.unregister(tagHandler);
         } catch (PersistentBus.EventBusException e) {
-            throw new RuntimeException("Unable to unregister to the EventBus!", e);
+            throw new RuntimeException("Failed to unregister bus handlers", e);
         }
         dateNotifier.stop();
     }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
index fc0cbb8..811c11c 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
@@ -46,7 +46,7 @@ public class DefaultInvoicePaymentApi implements InvoicePaymentApi {
 
     @Override
     public List<InvoicePayment> getInvoicePayments(final UUID paymentId, final TenantContext context) {
-        return ImmutableList.<InvoicePayment>copyOf(Collections2.transform(dao.getInvoicePayments(paymentId, internalCallContextFactory.createInternalTenantContext(paymentId, ObjectType.PAYMENT, context)),
+        return ImmutableList.<InvoicePayment>copyOf(Collections2.transform(dao.getInvoicePaymentsByPaymentId(paymentId, internalCallContextFactory.createInternalTenantContext(paymentId, ObjectType.PAYMENT, context)),
                                                                            new Function<InvoicePaymentModelDao, InvoicePayment>() {
                                                                                @Override
                                                                                public InvoicePayment apply(final InvoicePaymentModelDao input) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java b/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
index 8a5933b..2b02a73 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
@@ -103,7 +103,7 @@ public class InvoiceApiHelper {
             final List<InvoiceItemModelDao> createdInvoiceItems = dao.createInvoices(invoiceModelDaos, internalCallContext);
             return fromInvoiceItemModelDao(createdInvoiceItems);
         } catch (final LockFailedException e) {
-            log.error(String.format("Failed to process invoice items for account %s", accountId.toString()), e);
+            log.warn("Failed to process invoice items for accountId='{}'", accountId.toString(), e);
             return ImmutableList.<InvoiceItem>of();
         } finally {
             if (lock != null) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
index 56565d2..6cd672a 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
@@ -94,14 +94,16 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
     }
 
     @Override
-    public void notifyOfPayment(final UUID invoiceId, final BigDecimal amount, final Currency currency, final Currency processedCurrency, final UUID paymentId, final DateTime paymentDate, final boolean success, final InternalCallContext context) throws InvoiceApiException {
-        final InvoicePayment invoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, success);
-        notifyOfPayment(invoicePayment, context);
+    public void recordPaymentAttemptInit(final UUID invoiceId, final BigDecimal amount, final Currency currency, final Currency processedCurrency, final UUID paymentId, final String transactionExternalKey, final DateTime paymentDate, final InternalCallContext context) throws InvoiceApiException {
+        final InvoicePayment invoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, transactionExternalKey, false);
+        dao.notifyOfPaymentInit(new InvoicePaymentModelDao(invoicePayment), context);
     }
 
+
     @Override
-    public void notifyOfPayment(final InvoicePayment invoicePayment, final InternalCallContext context) throws InvoiceApiException {
-        dao.notifyOfPayment(new InvoicePaymentModelDao(invoicePayment), context);
+    public void recordPaymentAttemptCompletion(final UUID invoiceId, final BigDecimal amount, final Currency currency, final Currency processedCurrency, final UUID paymentId, final String transactionExternalKey, final DateTime paymentDate, final boolean success, final InternalCallContext context) throws InvoiceApiException {
+        final InvoicePayment invoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, transactionExternalKey, success);
+        dao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(invoicePayment), context);
     }
 
     @Override
@@ -121,7 +123,7 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
     }
 
     @Override
-    public InvoicePayment createRefund(final UUID paymentId, final BigDecimal amount, final boolean isInvoiceAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final String transactionExternalKey, final InternalCallContext context) throws InvoiceApiException {
+    public InvoicePayment recordRefund(final UUID paymentId, final BigDecimal amount, final boolean isInvoiceAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final String transactionExternalKey, final InternalCallContext context) throws InvoiceApiException {
         if (amount.compareTo(BigDecimal.ZERO) <= 0) {
             throw new InvoiceApiException(ErrorCode.PAYMENT_REFUND_AMOUNT_NEGATIVE_OR_NULL, paymentId, amount);
         }
@@ -144,7 +146,7 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
     }
 
     @Override
-    public InvoicePayment createChargeback(final UUID paymentId, final BigDecimal amount, final Currency currency, final InternalCallContext context) throws InvoiceApiException {
+    public InvoicePayment recordChargeback(final UUID paymentId, final BigDecimal amount, final Currency currency, final InternalCallContext context) throws InvoiceApiException {
         return new DefaultInvoicePayment(dao.postChargeback(paymentId, amount, currency, context));
     }
 
@@ -160,20 +162,15 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
     }
 
     private InvoicePayment getInvoicePayment(final UUID paymentId, final InvoicePaymentType type, final InternalTenantContext context) throws InvoiceApiException {
-        final Collection<InvoicePayment> invoicePayments = Collections2.transform(dao.getInvoicePayments(paymentId, context), new Function<InvoicePaymentModelDao, InvoicePayment>() {
-            @Override
-            public InvoicePayment apply(final InvoicePaymentModelDao input) {
-                return new DefaultInvoicePayment(input);
-            }
-        });
-        if (invoicePayments.isEmpty()) {
-            return null;
-        }
-        return Iterables.tryFind(invoicePayments, new Predicate<InvoicePayment>() {
+
+        final List<InvoicePaymentModelDao> invoicePayments = dao.getInvoicePaymentsByPaymentId(paymentId, context);
+        final InvoicePaymentModelDao resultOrNull = Iterables.tryFind(invoicePayments, new Predicate<InvoicePaymentModelDao>() {
             @Override
-            public boolean apply(final InvoicePayment input) {
-                return input.getType() == type;
+            public boolean apply(final InvoicePaymentModelDao input) {
+                return input.getType() == type &&
+                        input.getSuccess();
             }
         }).orNull();
+        return resultOrNull != null ? new DefaultInvoicePayment(resultOrNull) : null;
     }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
index 7e7406c..c7ba438 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -456,10 +456,11 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
     }
 
     private void notifyBusOfInvoiceAdjustment(final UUID invoiceId, final UUID accountId, final InternalCallContext context) {
+        final DefaultInvoiceAdjustmentEvent event = new DefaultInvoiceAdjustmentEvent(invoiceId, accountId, context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
         try {
-            eventBus.post(new DefaultInvoiceAdjustmentEvent(invoiceId, accountId, context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()));
+            eventBus.post(event);
         } catch (final EventBusException e) {
-            log.warn("Failed to post adjustment event for invoice " + invoiceId, e);
+            log.warn("Failed to post event {}", event, e);
         }
     }
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index 1aa7fd8..f12284a 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -411,7 +411,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
     }
 
     @Override
-    public List<InvoicePaymentModelDao> getInvoicePayments(final UUID paymentId, final InternalTenantContext context) {
+    public List<InvoicePaymentModelDao> getInvoicePaymentsByPaymentId(final UUID paymentId, final InternalTenantContext context) {
         return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoicePaymentModelDao>>() {
             @Override
             public List<InvoicePaymentModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
@@ -447,7 +447,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                 final InvoicePaymentModelDao payment = Iterables.tryFind(paymentsForId, new Predicate<InvoicePaymentModelDao>() {
                     @Override
                     public boolean apply(final InvoicePaymentModelDao input) {
-                        return input.getType() == InvoicePaymentType.ATTEMPT;
+                        return input.getType() == InvoicePaymentType.ATTEMPT && input.getSuccess();
                     }
                 }).orNull();
                 if (payment == null) {
@@ -672,32 +672,61 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
         });
     }
 
+
+    @Override
+    public void notifyOfPaymentInit(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
+        notifyOfPaymentCompletionInternal(invoicePayment, false, context);
+    }
+
+
     @Override
-    public void notifyOfPayment(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
+    public void notifyOfPaymentCompletion(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
+        notifyOfPaymentCompletionInternal(invoicePayment, true, context);
+    }
+
+    public void notifyOfPaymentCompletionInternal(final InvoicePaymentModelDao invoicePayment, final boolean completion, final InternalCallContext context) {
         transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
             @Override
             public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
                 final InvoicePaymentSqlDao transactional = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class);
-
-                // If the payment id is null, the payment wasn't attempted (e.g. no payment method). We don't record an attempt but send an event nonetheless (e.g. for Overdue)
-                if (invoicePayment.getPaymentId() != null) {
-                    final List<InvoicePaymentModelDao> invoicePayments = transactional.getInvoicePayments(invoicePayment.getPaymentId().toString(), context);
+                //
+                // In case of notifyOfPaymentInit we always want to record the row with success = false
+                // Otherwise, if the payment id is null, the payment wasn't attempted (e.g. no payment method so we don't record an attempt but send
+                // an event nonetheless (e.g. for Overdue)
+                //
+                if (!completion || invoicePayment.getPaymentId() != null) {
+                    //
+                    // extract entries by invoiceId (which is always set, as opposed to paymentId) and then filter based on type and
+                    // paymentCookieId = transactionExternalKey
+                    //
+                    final List<InvoicePaymentModelDao> invoicePayments = transactional.getPaymentsForInvoice(invoicePayment.getInvoiceId().toString(), context);
                     final InvoicePaymentModelDao existingAttempt = Iterables.tryFind(invoicePayments, new Predicate<InvoicePaymentModelDao>() {
                         @Override
                         public boolean apply(final InvoicePaymentModelDao input) {
-                            return input.getType() == InvoicePaymentType.ATTEMPT;
+                            return input.getType() == InvoicePaymentType.ATTEMPT &&
+                                   input.getPaymentCookieId().equals(invoicePayment.getPaymentCookieId());
                         }
                     }).orNull();
+
                     if (existingAttempt == null) {
                         transactional.create(invoicePayment, context);
                     } else if (!existingAttempt.getSuccess() && invoicePayment.getSuccess()) {
-                        transactional.updateAttempt(existingAttempt.getRecordId(), invoicePayment.getPaymentDate().toDate(), invoicePayment.getAmount(), invoicePayment.getCurrency(), invoicePayment.getProcessedCurrency(), context);
+                        transactional.updateAttempt(existingAttempt.getRecordId(),
+                                                    invoicePayment.getPaymentId().toString(),
+                                                    invoicePayment.getPaymentDate().toDate(),
+                                                    invoicePayment.getAmount(),
+                                                    invoicePayment.getCurrency(),
+                                                    invoicePayment.getProcessedCurrency(),
+                                                    invoicePayment.getPaymentCookieId(),
+                                                    null,
+                                                    context);
                     }
                 }
 
-                final UUID accountId = nonEntityDao.retrieveIdFromObjectInTransaction(context.getAccountRecordId(), ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID), entitySqlDaoWrapperFactory.getHandle());
-                notifyBusOfInvoicePayment(entitySqlDaoWrapperFactory, invoicePayment, accountId, context.getUserToken(), context);
-
+                if (completion) {
+                    final UUID accountId = nonEntityDao.retrieveIdFromObjectInTransaction(context.getAccountRecordId(), ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID), entitySqlDaoWrapperFactory.getHandle());
+                    notifyBusOfInvoicePayment(entitySqlDaoWrapperFactory, invoicePayment, accountId, context.getUserToken(), context);
+                }
                 return null;
             }
         });
@@ -848,7 +877,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
             eventBus.postFromTransaction(new DefaultInvoiceAdjustmentEvent(invoiceId, accountId, context.getAccountRecordId(), context.getTenantRecordId(), userToken),
                                          entitySqlDaoWrapperFactory.getHandle().getConnection());
         } catch (final EventBusException e) {
-            log.warn("Failed to post adjustment event for invoice " + invoiceId, e);
+            log.warn("Failed to post adjustment event for invoiceId='{}'", invoiceId, e);
         }
     }
 
@@ -887,7 +916,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
         try {
             eventBus.postFromTransaction(busEvent, entitySqlDaoWrapperFactory.getHandle().getConnection());
         } catch (final EventBusException e) {
-            log.warn("Failed to post invoice payment event for invoice " + invoicePaymentModelDao.getInvoiceId(), e);
+            log.warn("Failed to post invoice payment event for invoiceId='{}'", invoicePaymentModelDao.getInvoiceId(), e);
         }
     }
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
index effce52..3ee47be 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
@@ -32,6 +32,7 @@ import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoicePayment;
 import org.killbill.billing.util.entity.Pagination;
 import org.killbill.billing.util.entity.dao.EntityDao;
 
@@ -58,7 +59,7 @@ public interface InvoiceDao extends EntityDao<InvoiceModelDao, Invoice, InvoiceA
 
     UUID getInvoiceIdByPaymentId(UUID paymentId, InternalTenantContext context);
 
-    List<InvoicePaymentModelDao> getInvoicePayments(UUID paymentId, InternalTenantContext context);
+    List<InvoicePaymentModelDao> getInvoicePaymentsByPaymentId(UUID paymentId, InternalTenantContext context);
 
     List<InvoicePaymentModelDao> getInvoicePaymentsByAccount(InternalTenantContext context);
 
@@ -131,7 +132,9 @@ public interface InvoiceDao extends EntityDao<InvoiceModelDao, Invoice, InvoiceA
      */
     void deleteCBA(UUID accountId, UUID invoiceId, UUID invoiceItemId, InternalCallContext context) throws InvoiceApiException;
 
-    void notifyOfPayment(InvoicePaymentModelDao invoicePayment, InternalCallContext context);
+    void notifyOfPaymentInit(InvoicePaymentModelDao invoicePayment, InternalCallContext context);
+
+    void notifyOfPaymentCompletion(InvoicePaymentModelDao invoicePayment, InternalCallContext context);
 
     /**
      * @param accountId the account for which we need to rebalance the CBA
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
index 279aea3..7b8c5ad 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
@@ -69,12 +69,14 @@ public interface InvoicePaymentSqlDao extends EntitySqlDao<InvoicePaymentModelDa
                                                            @BindBean final InternalTenantContext context);
 
 
-
     @SqlUpdate
     void updateAttempt(@Bind("recordId") Long recordId,
+                       @Bind("paymentId") final String paymentId,
                        @Bind("paymentDate") final Date paymentDate,
                        @Bind("amount") final BigDecimal amount,
                        @Bind("currency") final Currency currency,
                        @Bind("processedCurrency") final Currency processedCurrency,
+                       @Bind("paymentCookieId") final String paymentCookieId,
+                       @Bind("linkedInvoicePaymentId") final String linkedInvoicePaymentId,
                        @BindBean final InternalTenantContext context);
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
index 27e6a43..9650e8c 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -103,10 +103,11 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator 
         }
 
         // Pretty-print the generated invoice items from the junction events
-        final StringBuilder logStringBuilder = new StringBuilder("Proposed Invoice items for invoiceId ")
+        final StringBuilder logStringBuilder = new StringBuilder("Proposed Invoice items for invoiceId='")
                 .append(invoiceId)
-                .append(" and accountId ")
-                .append(accountId);
+                .append("', accountId='")
+                .append(accountId)
+                .append("'");
 
         final Iterator<BillingEvent> eventIt = events.iterator();
         BillingEvent nextEvent = eventIt.next();
@@ -201,7 +202,7 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator 
                     }
                     final BigDecimal rate = thisEvent.getRecurringPrice();
                     if (rate != null) {
-                        final BigDecimal amount = KillBillMoney.of(itemDatum.getNumberOfCycles().multiply(rate), currency);
+                        final BigDecimal amount = itemDatum.getNumberOfCycles().multiply(rate);
                         final RecurringInvoiceItem recurringItem = new RecurringInvoiceItem(invoiceId,
                                                                                             accountId,
                                                                                             thisEvent.getSubscription().getBundleId(),
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index cf7c200..d46a0ed 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -182,7 +182,7 @@ public class InvoiceDispatcher {
             try {
                 eventBus.post(event);
             } catch (EventBusException e) {
-                log.error("Failed to post event " + event, e);
+                log.warn("Failed to post event {}", event, e);
             }
         }
     }
@@ -190,7 +190,7 @@ public class InvoiceDispatcher {
     private Invoice processSubscriptionInternal(final UUID subscriptionId, final DateTime targetDate, final boolean dryRunForNotification, final InternalCallContext context) throws InvoiceApiException {
         try {
             if (subscriptionId == null) {
-                log.error("Failed handling SubscriptionBase change.", new InvoiceApiException(ErrorCode.INVOICE_INVALID_TRANSITION));
+                log.warn("Failed handling SubscriptionBase change.", new InvoiceApiException(ErrorCode.INVOICE_INVALID_TRANSITION));
                 return null;
             }
             final UUID accountId = subscriptionApi.getAccountIdFromSubscriptionId(subscriptionId, context);
@@ -198,7 +198,7 @@ public class InvoiceDispatcher {
 
             return processAccount(accountId, targetDate, dryRunArguments, context);
         } catch (final SubscriptionBaseApiException e) {
-            log.error("Failed handling SubscriptionBase change.",
+            log.warn("Failed handling SubscriptionBase change.",
                       new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, subscriptionId.toString()));
             return null;
         }
@@ -212,9 +212,7 @@ public class InvoiceDispatcher {
 
             return processAccountWithLock(accountId, targetDate, dryRunArguments, context);
         } catch (final LockFailedException e) {
-            // Not good!
-            log.error(String.format("Failed to process invoice for account %s, targetDate %s",
-                                    accountId.toString(), targetDate), e);
+            log.warn("Failed to process invoice for accountId='{}', targetDate='{}'", accountId.toString(), targetDate, e);
         } finally {
             if (lock != null) {
                 lock.release();
@@ -324,15 +322,15 @@ public class InvoiceDispatcher {
             //
             if (invoice == null) {
                 if (isDryRun) {
-                    log.info("Generated null dryRun invoice for accountId {} and targetDate {} (targetDateTime {})", new Object[]{accountId, targetDate, targetDateTime});
+                    log.info("Generated null dryRun invoice for accountId='{}', targetDate='{}', targetDateTime='{}'", accountId, targetDate, targetDateTime);
                 } else {
-                    log.info("Generated null invoice for accountId {} and targetDate {} (targetDateTime {})", new Object[]{accountId, targetDate, targetDateTime});
+                    log.info("Generated null invoice for accountId='{}', targetDate='{}', targetDateTime='{}'", accountId, targetDate, targetDateTime);
 
                     final BusInternalEvent event = new DefaultNullInvoiceEvent(accountId, clock.getUTCToday(),
                                                                                context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
 
                     commitInvoiceAndSetFutureNotifications(account, null, ImmutableList.<InvoiceItemModelDao>of(), futureAccountNotifications, false, context);
-                    postEvent(event, accountId, context);
+                    postEvent(event);
                 }
                 return null;
             }
@@ -451,10 +449,10 @@ public class InvoiceDispatcher {
     private void logInvoiceWithItems(final ImmutableAccountData account, final Invoice invoice, final LocalDate targetDate, final Set<UUID> adjustedUniqueOtherInvoiceId, final boolean isRealInvoiceWithItems) {
         final StringBuilder tmp = new StringBuilder();
         if (isRealInvoiceWithItems) {
-            tmp.append(String.format("Generated invoice %s with %d items for accountId %s and targetDate %s:\n", invoice.getId(), invoice.getNumberOfItems(), account.getId(), targetDate));
+            tmp.append(String.format("Generated invoiceId='%s', numberOfItems='%d', accountId='%s', targetDate='%s':\n", invoice.getId(), invoice.getNumberOfItems(), account.getId(), targetDate));
         } else {
             final String adjustedInvoices = JOINER_COMMA.join(adjustedUniqueOtherInvoiceId.toArray(new UUID[adjustedUniqueOtherInvoiceId.size()]));
-            tmp.append(String.format("Adjusting existing invoices %s with %d items for accountId %s and targetDate %s:\n",
+            tmp.append(String.format("Adjusting existing invoiceId='%s', numberOfItems='%d', accountId='%s', targetDate='%s':\n",
                                      adjustedInvoices, invoice.getNumberOfItems(), account.getId(), targetDate));
         }
         for (InvoiceItem item : invoice.getInvoiceItems()) {
@@ -499,7 +497,7 @@ public class InvoiceDispatcher {
             events.add(event);
         }
         for (final InvoiceInternalEvent event : events) {
-            postEvent(event, account.getId(), context);
+            postEvent(event);
         }
     }
 
@@ -554,11 +552,11 @@ public class InvoiceDispatcher {
         }
     }
 
-    private void postEvent(final BusInternalEvent event, final UUID accountId, final InternalCallContext context) {
+    private void postEvent(final BusInternalEvent event) {
         try {
             eventBus.post(event);
         } catch (final EventBusException e) {
-            log.error(String.format("Failed to post event %s for account %s", event.getBusEventType(), accountId), e);
+            log.warn("Failed to post event {}", event, e);
         }
     }
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
index ca63e2b..0a82d37 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
@@ -19,23 +19,21 @@ package org.killbill.billing.invoice;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import org.killbill.billing.account.api.AccountApiException;
 import org.killbill.billing.account.api.AccountInternalApi;
-import org.killbill.clock.Clock;
+import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.events.BlockingTransitionInternalEvent;
-import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.events.EffectiveEntitlementInternalEvent;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
 import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
 import org.killbill.billing.util.callcontext.CallOrigin;
-import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.callcontext.UserType;
-import org.killbill.billing.events.EffectiveEntitlementInternalEvent;
-import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
-import org.killbill.billing.events.RepairSubscriptionInternalEvent;
 import org.killbill.billing.util.config.InvoiceConfig;
+import org.killbill.clock.Clock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.eventbus.AllowConcurrentEvents;
 import com.google.common.eventbus.Subscribe;
@@ -63,18 +61,6 @@ public class InvoiceListener {
 
     @AllowConcurrentEvents
     @Subscribe
-    public void handleRepairSubscriptionEvent(final RepairSubscriptionInternalEvent event) {
-
-        try {
-            final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "RepairBundle", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
-            dispatcher.processAccount(event.getAccountId(), event.getEffectiveDate(), null, context);
-        } catch (InvoiceApiException e) {
-            log.error(e.getMessage());
-        }
-    }
-
-    @AllowConcurrentEvents
-    @Subscribe
     public void handleSubscriptionTransition(final EffectiveSubscriptionInternalEvent event) {
 
         try {
@@ -88,7 +74,7 @@ public class InvoiceListener {
             final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "SubscriptionBaseTransition", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
             dispatcher.processSubscriptionForInvoiceGeneration(event, context);
         } catch (InvoiceApiException e) {
-            log.error(e.getMessage());
+            log.warn("Unable to process event {}", event, e);
         }
     }
 
@@ -100,7 +86,7 @@ public class InvoiceListener {
             final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "SubscriptionBaseTransition", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
             dispatcher.processAccount(event.getAccountId(), event.getEffectiveTransitionTime(), null, context);
         } catch (InvoiceApiException e) {
-            log.error(e.getMessage());
+            log.warn("Unable to process event {}", event, e);
         }
     }
 
@@ -118,9 +104,9 @@ public class InvoiceListener {
             final UUID accountId = accountApi.getByRecordId(event.getSearchKey1(), context);
             dispatcher.processAccount(accountId, clock.getUTCNow(), null, context);
         } catch (InvoiceApiException e) {
-            log.error(e.getMessage());
+            log.warn("Unable to process event {}", event, e);
         } catch (AccountApiException e) {
-            log.error(e.getMessage());
+            log.warn("Unable to process event {}", event, e);
         }
     }
 
@@ -129,7 +115,7 @@ public class InvoiceListener {
             final InternalCallContext context = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "Next Billing Date", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
             dispatcher.processSubscriptionForInvoiceGeneration(subscriptionId, eventDateTime, context);
         } catch (InvoiceApiException e) {
-            log.error(e.getMessage());
+            log.warn("Unable to process subscriptionId='{}', eventDateTime='{}'", subscriptionId, eventDateTime, e);
         }
     }
 
@@ -138,7 +124,7 @@ public class InvoiceListener {
             final InternalCallContext context = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "Next Billing Date", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
             dispatcher.processSubscriptionForInvoiceNotification(subscriptionId, eventDateTime, context);
         } catch (InvoiceApiException e) {
-            log.error(e.getMessage());
+            log.warn("Unable to process subscriptionId='{}', eventDateTime='{}'", subscriptionId, eventDateTime, e);
         }
     }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
index cb6ed8c..facb7ba 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
@@ -67,7 +67,7 @@ public class InvoiceTagHandler {
         try {
             dispatcher.processAccount(accountId, clock.getUTCNow(), null, context);
         } catch (InvoiceApiException e) {
-            log.warn(String.format("Failed to process process removal AUTO_INVOICING_OFF for account %s", accountId), e);
+            log.warn("Failed to process tag removal AUTO_INVOICING_OFF for accountId='{}'", accountId, e);
         }
     }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java
index 54363ab..8402c5b 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java
@@ -44,8 +44,8 @@ public class DefaultInvoicePayment extends EntityBase implements InvoicePayment 
     private final Boolean isSuccess;
 
     public DefaultInvoicePayment(final InvoicePaymentType type, final UUID paymentId, final UUID invoiceId, final DateTime paymentDate,
-                                 final BigDecimal amount, final Currency currency, final Currency processedCurrency, final Boolean isSuccess) {
-        this(UUIDs.randomUUID(), null, type, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, null, null, isSuccess);
+                                 final BigDecimal amount, final Currency currency, final Currency processedCurrency, final String paymentCookieId, final Boolean isSuccess) {
+        this(UUIDs.randomUUID(), null, type, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, paymentCookieId, null, isSuccess);
     }
 
     public DefaultInvoicePayment(final UUID id, final InvoicePaymentType type, final UUID paymentId, final UUID invoiceId, final DateTime paymentDate,
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
index b3f12d8..f143825 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2010-2014 Ning, Inc.
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -27,6 +29,7 @@ import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.entity.EntityBase;
 import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.util.currency.KillBillMoney;
 
 public abstract class InvoiceItemBase extends EntityBase implements InvoiceItem {
 
@@ -109,7 +112,7 @@ public abstract class InvoiceItemBase extends EntityBase implements InvoiceItem 
         this.usageName = usageName;
         this.startDate = startDate;
         this.endDate = endDate;
-        this.amount = amount;
+        this.amount = amount == null || currency == null ? amount : KillBillMoney.of(amount, currency);
         this.currency = currency;
         this.rate = rate;
         this.linkedItemId = reversedItemId;
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java
index e5ec2ae..05e661f 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java
@@ -70,33 +70,29 @@ public class DefaultNextBillingDateNotifier implements NextBillingDateNotifier {
         final NotificationQueueHandler notificationQueueHandler = new NotificationQueueHandler() {
             @Override
             public void handleReadyNotification(final NotificationEvent notificationKey, final DateTime eventDate, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+                if (!(notificationKey instanceof NextBillingDateNotificationKey)) {
+                    log.error("Invoice service received an unexpected event className='{}", notificationKey.getClass());
+                    return;
+                }
+
+                final NextBillingDateNotificationKey key = (NextBillingDateNotificationKey) notificationKey;
+
+                // Just to ensure compatibility with json that might not have that targetDate field (old versions < 0.13.6)
+                final DateTime targetDate = key.getTargetDate() != null ? key.getTargetDate() : eventDate;
                 try {
-                    if (!(notificationKey instanceof NextBillingDateNotificationKey)) {
-                        log.error("Invoice service received an unexpected event type {}", notificationKey.getClass().getName());
+                    final SubscriptionBase subscription = subscriptionApi.getSubscriptionFromId(key.getUuidKey(), callContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId));
+                    if (subscription == null) {
+                        log.warn("Unable to retrieve subscriptionId='{}' for event {}", key.getUuidKey(), key);
                         return;
                     }
-
-                    final NextBillingDateNotificationKey key = (NextBillingDateNotificationKey) notificationKey;
-
-                    // Just to ensure compatibility with json that might not have that targetDate field (old versions < 0.13.6)
-                    final DateTime targetDate = key.getTargetDate() != null ? key.getTargetDate() : eventDate;
-                    try {
-                        final SubscriptionBase subscription = subscriptionApi.getSubscriptionFromId(key.getUuidKey(), callContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId));
-                        if (subscription == null) {
-                            log.warn("Next Billing Date Notification Queue handled spurious notification (key: " + key + ")");
-                            return;
-                        }
-                        if (key.isDryRunForInvoiceNotification() != null && // Just to ensure compatibility with json that might not have that field (old versions < 0.13.6)
-                            key.isDryRunForInvoiceNotification()) {
-                            processEventForInvoiceNotification(key.getUuidKey(), targetDate, userToken, accountRecordId, tenantRecordId);
-                        } else {
-                            processEventForInvoiceGeneration(key.getUuidKey(), targetDate, userToken, accountRecordId, tenantRecordId);
-                        }
-                    } catch (SubscriptionBaseApiException e) {
-                        log.warn("Next Billing Date Notification Queue handled spurious notification (key: " + key + ")", e);
+                    if (key.isDryRunForInvoiceNotification() != null && // Just to ensure compatibility with json that might not have that field (old versions < 0.13.6)
+                        key.isDryRunForInvoiceNotification()) {
+                        processEventForInvoiceNotification(key.getUuidKey(), targetDate, userToken, accountRecordId, tenantRecordId);
+                    } else {
+                        processEventForInvoiceGeneration(key.getUuidKey(), targetDate, userToken, accountRecordId, tenantRecordId);
                     }
-                } catch (IllegalArgumentException e) {
-                    log.error("The key returned from the NextBillingNotificationQueue is not a valid UUID", e);
+                } catch (SubscriptionBaseApiException e) {
+                    log.warn("Error retrieving subscriptionId='{}'", key.getUuidKey(), e);
                 }
             }
         };
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultInvoiceProviderPluginRegistry.java b/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultInvoiceProviderPluginRegistry.java
index 404b9fc..1860257 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultInvoiceProviderPluginRegistry.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultInvoiceProviderPluginRegistry.java
@@ -41,13 +41,13 @@ public class DefaultInvoiceProviderPluginRegistry implements OSGIServiceRegistra
 
     @Override
     public void registerService(final OSGIServiceDescriptor desc, final InvoicePluginApi service) {
-        log.info("DefaultInvoiceProviderPluginRegistry registering service " + desc.getRegistrationName());
+        log.info("Registering service='{}'", desc.getRegistrationName());
         pluginsByName.put(desc.getRegistrationName(), service);
     }
 
     @Override
     public void unregisterService(final String serviceName) {
-        log.info("DefaultInvoiceProviderPluginRegistry unregistering service " + serviceName);
+        log.info("Unregistering service='{}'", serviceName);
         pluginsByName.remove(serviceName);
     }
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
index 7ebb8d2..6f38c8f 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
@@ -284,10 +284,10 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
                 }
             }
         } catch (final CurrencyConversionException e) {
-            logger.warn("Failed to retrieve currency conversion rates for currency = " + currency + " and date = " + latestPaymentDate, e);
+            logger.warn("Failed to retrieve currency conversion rates for currency='{}', dateConversion='{}'", currency, latestPaymentDate, e);
             return null;
         }
-        logger.warn("Failed to retrieve currency conversion rates for currency = " + currency + " and date = " + latestPaymentDate);
+        logger.warn("Failed to retrieve currency conversion rates for currency='{}', dateConversion='{}'", currency, latestPaymentDate);
         return null;
     }
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java
index f164b76..c5dd0d3 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java
@@ -128,13 +128,13 @@ public class Item {
                                                                      .multiply(amount) : amount;
 
         if (action == ItemAction.ADD) {
-            return new RecurringInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, newStartDate, newEndDate, KillBillMoney.of(positiveAmount, currency), rate, currency);
+            return new RecurringInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, newStartDate, newEndDate, positiveAmount, rate, currency);
         } else {
             // We first compute the maximum amount after adjustment and that sets the amount limit of how much can be repaired.
             final BigDecimal maxAvailableAmountAfterAdj = amount.subtract(adjustedAmount);
             final BigDecimal maxAvailableAmountForRepair = maxAvailableAmountAfterAdj.subtract(currentRepairedAmount);
             final BigDecimal positiveAmountForRepair = positiveAmount.compareTo(maxAvailableAmountForRepair) <= 0 ? positiveAmount : maxAvailableAmountForRepair;
-            return positiveAmountForRepair.compareTo(BigDecimal.ZERO) > 0 ? new RepairAdjInvoiceItem(targetInvoiceId, accountId, newStartDate, newEndDate, KillBillMoney.of(positiveAmountForRepair.negate(), currency), currency, linkedId) : null;
+            return positiveAmountForRepair.compareTo(BigDecimal.ZERO) > 0 ? new RepairAdjInvoiceItem(targetInvoiceId, accountId, newStartDate, newEndDate, positiveAmountForRepair.negate(), currency, linkedId) : null;
         }
     }
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/RawUsageOptimizer.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/RawUsageOptimizer.java
index afdc98d..b78fcbc 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/RawUsageOptimizer.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/RawUsageOptimizer.java
@@ -67,8 +67,8 @@ public class RawUsageOptimizer {
 
     public RawUsageOptimizerResult getConsumableInArrearUsage(final LocalDate firstEventStartDate, final LocalDate targetDate, final Iterable<InvoiceItem> existingUsageItems, final Map<String, Usage> knownUsage, final InternalCallContext internalCallContext) {
         final LocalDate targetStartDate = config.getMaxRawUsagePreviousPeriod() > 0 ? getOptimizedRawUsageStartDate(firstEventStartDate, targetDate, existingUsageItems, knownUsage) : firstEventStartDate;
-        log.info("RawUsageOptimizer [accountRecordId = {}]: rawUsageStartDate = {}, (proposed) firstEventStartDate = {}",
-                 new Object[]{internalCallContext.getAccountRecordId(), targetStartDate, firstEventStartDate});
+        log.info("ConsumableInArrear accountRecordId='{}', rawUsageStartDate='{}', firstEventStartDate='{}'",
+                 internalCallContext.getAccountRecordId(), targetStartDate, firstEventStartDate);
 
         final List<RawUsage> rawUsageData = usageApi.getRawUsageForAccount(targetStartDate, targetDate, internalCallContext);
         return new RawUsageOptimizerResult(firstEventStartDate, targetStartDate, rawUsageData);
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
index b9e4dab..9925595 100644
--- a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
@@ -109,10 +109,14 @@ getChargebacksByPaymentId() ::= <<
 
 updateAttempt() ::= <<
     UPDATE <tableName()>
-    SET success = true,
+    SET payment_id := :paymentId,
     payment_date = :paymentDate,
     amount = :amount,
-    processed_currency = :processedCurrency
+    currency = :currency,
+    processed_currency = :processedCurrency,
+    payment_cookie_id = :paymentCookieId,
+    linked_invoice_payment_id := :linkedInvoicePaymentId,
+    success = true
     WHERE record_id = :recordId
     <AND_CHECK_TENANT("")>
     ;
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java b/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
index 0af1018..8b7bbfb 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
@@ -114,7 +114,7 @@ public class TestDefaultInvoicePaymentApi extends InvoiceTestSuiteWithEmbeddedDB
         Assert.assertEquals(initialInvoiceBalance.compareTo(BigDecimal.ZERO), 0);
 
         // Create a full refund with no adjustment
-        final InvoicePayment refund = invoiceInternalApi.createRefund(payment.getPaymentId(), refundAmount, adjusted, invoiceItemIdsWithAmounts,
+        final InvoicePayment refund = invoiceInternalApi.recordRefund(payment.getPaymentId(), refundAmount, adjusted, invoiceItemIdsWithAmounts,
                                                                       UUID.randomUUID().toString(), internalCallContext);
         Assert.assertEquals(refund.getAmount().compareTo(refundAmount.negate()), 0);
         Assert.assertEquals(refund.getCurrency(), CURRENCY);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
index 2fd989a..a98acae 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
@@ -229,7 +229,7 @@ public class TestDefaultInvoiceUserApi extends InvoiceTestSuiteWithEmbeddedDB {
         Assert.assertEquals(accountBalance, invoiceBalance);
 
         // Adjust the invoice for a fraction of the balance
-        final BigDecimal creditAmount = invoiceBalance.divide(BigDecimal.TEN);
+        final BigDecimal creditAmount = invoiceBalance.divide(BigDecimal.TEN, BigDecimal.ROUND_HALF_UP);
         final InvoiceItem creditInvoiceItem = invoiceUserApi.insertCreditForInvoice(accountId, invoiceId, creditAmount,
                                                                                     clock.getUTCToday(), accountCurrency, callContext);
         Assert.assertEquals(creditInvoiceItem.getInvoiceId(), invoiceId);
@@ -306,7 +306,7 @@ public class TestDefaultInvoiceUserApi extends InvoiceTestSuiteWithEmbeddedDB {
         Assert.assertEquals(accountBalance, invoiceBalance);
 
         // Adjust the invoice for a fraction of the balance
-        final BigDecimal adjAmount = invoiceItem.getAmount().divide(BigDecimal.TEN);
+        final BigDecimal adjAmount = invoiceItem.getAmount().divide(BigDecimal.TEN, BigDecimal.ROUND_HALF_UP);
         final InvoiceItem adjInvoiceItem = invoiceUserApi.insertInvoiceItemAdjustment(accountId, invoiceId, invoiceItem.getId(),
                                                                                       clock.getUTCToday(), adjAmount, accountCurrency,
                                                                                       callContext);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
index 6c1c27e..2164386 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
@@ -27,7 +27,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
-import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
@@ -213,7 +212,7 @@ public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice, 
     }
 
     @Override
-    public List<InvoicePaymentModelDao> getInvoicePayments(final UUID paymentId, final InternalTenantContext context) {
+    public List<InvoicePaymentModelDao> getInvoicePaymentsByPaymentId(final UUID paymentId, final InternalTenantContext context) {
         final List<InvoicePaymentModelDao> result = new LinkedList<InvoicePaymentModelDao>();
         synchronized (monitor) {
             for (final InvoicePaymentModelDao payment : payments.values()) {
@@ -243,7 +242,7 @@ public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice, 
     }
 
     @Override
-    public void notifyOfPayment(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
+    public void notifyOfPaymentCompletion(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
         synchronized (monitor) {
             payments.put(invoicePayment.getId(), invoicePayment);
         }
@@ -362,4 +361,11 @@ public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice, 
     public void deleteCBA(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId, final InternalCallContext context) throws InvoiceApiException {
         throw new UnsupportedOperationException();
     }
+
+    @Override
+    public void notifyOfPaymentInit(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
+        synchronized (monitor) {
+            payments.put(invoicePayment.getId(), invoicePayment);
+        }
+    }
 }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
index dc87e42..6bbfbe3 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
@@ -32,7 +32,6 @@ import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.Account;
-import org.killbill.billing.account.api.DefaultAccount;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.catalog.DefaultPrice;
 import org.killbill.billing.catalog.MockInternationalPrice;
@@ -151,8 +150,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         final BigDecimal paymentAmount = new BigDecimal("11.00");
         final UUID paymentId = UUID.randomUUID();
 
-        final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, clock.getUTCNow().plusDays(12), paymentAmount, Currency.USD, Currency.USD, true);
-        invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context);
+        final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, clock.getUTCNow().plusDays(12), paymentAmount, Currency.USD, Currency.USD, "cookie", true);
+        invoiceDao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(defaultInvoicePayment), context);
 
         final InvoiceModelDao retrievedInvoice = invoiceDao.getById(invoiceId, context);
         assertNotNull(retrievedInvoice);
@@ -522,7 +521,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         invoiceUtil.createInvoiceItem(item2, context);
 
         final BigDecimal payment1 = new BigDecimal("48.0");
-        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, true);
+        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, null, true);
         invoiceUtil.createPayment(payment, context);
 
         final BigDecimal balance = invoiceDao.getAccountBalance(accountId, context);
@@ -587,7 +586,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         invoiceUtil.createInvoice(invoice1, true, context);
 
         final BigDecimal payment1 = new BigDecimal("48.0");
-        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, true);
+        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, null, true);
         invoiceUtil.createPayment(payment, context);
 
         final BigDecimal balance = invoiceDao.getAccountBalance(accountId, context);
@@ -628,7 +627,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         // Pay the whole thing
         final UUID paymentId = UUID.randomUUID();
         final BigDecimal payment1 = rate1;
-        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, true);
+        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, null, true);
         invoiceUtil.createPayment(payment, context);
         balance = invoiceDao.getAccountBalance(accountId, context);
         assertEquals(balance.compareTo(new BigDecimal("0.00")), 0);
@@ -677,7 +676,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         // Pay the whole thing
         final UUID paymentId = UUID.randomUUID();
         final BigDecimal payment1 = amount;
-        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, true);
+        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, null, true);
         invoiceUtil.createPayment(payment, context);
         balancePriorRefund = invoiceDao.getAccountBalance(accountId, context);
         assertEquals(balancePriorRefund.compareTo(new BigDecimal("0.00")), 0);
@@ -782,7 +781,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         // Pay the whole thing
         final UUID paymentId = UUID.randomUUID();
         final BigDecimal payment1 = amount1.add(rate1);
-        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, true);
+        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, null, true);
         invoiceUtil.createPayment(payment, context);
         balance = invoiceDao.getAccountBalance(accountId, context);
         assertEquals(balance.compareTo(new BigDecimal("0.00")), 0);
@@ -873,7 +872,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
 
         // Pay the whole thing
         final BigDecimal payment1 = amount1.add(rate1);
-        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, true);
+        final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, null, true);
         invoiceUtil.createPayment(payment, context);
         balance = invoiceDao.getAccountBalance(accountId, context);
         assertEquals(balance.compareTo(new BigDecimal("0.00")), 0);
@@ -1324,8 +1323,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         // SECOND CREATE THE PAYMENT
         final BigDecimal paymentAmount = new BigDecimal("239.00");
         final UUID paymentId = UUID.randomUUID();
-        final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, clock.getUTCNow(), paymentAmount, Currency.USD, Currency.USD, true);
-        invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context);
+        final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, clock.getUTCNow(), paymentAmount, Currency.USD, Currency.USD, "cookie", true);
+        invoiceDao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(defaultInvoicePayment), context);
 
         // AND THEN THIRD THE REFUND
         final Map<UUID, BigDecimal> invoiceItemMap = new HashMap<UUID, BigDecimal>();
@@ -1474,9 +1473,9 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
 
         final UUID paymentId = UUID.randomUUID();
         final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), clock.getUTCNow().plusDays(12), new BigDecimal("10.0"),
-                                                                                      Currency.USD, Currency.USD, true);
+                                                                                      Currency.USD, Currency.USD, "cookie", true);
 
-        invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context);
+        invoiceDao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(defaultInvoicePayment), context);
 
         // Create invoice 2
         // Scenario: single item
@@ -1535,8 +1534,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         final UUID paymentId = UUID.randomUUID();
 
         final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), clock.getUTCNow().plusDays(12), paymentAmount,
-                                                                                      Currency.USD, Currency.USD, true);
-        invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context);
+                                                                                      Currency.USD, Currency.USD, "cookie", true);
+        invoiceDao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(defaultInvoicePayment), context);
 
         // Create invoice 2
         // Scenario: single item
@@ -1636,15 +1635,15 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
 
 
         final UUID paymentId = UUID.randomUUID();
-        final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), clock.getUTCNow().plusDays(12), BigDecimal.TEN, Currency.USD, Currency.USD, false);
-        invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context);
+        final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), clock.getUTCNow().plusDays(12), BigDecimal.TEN, Currency.USD, Currency.USD, "cookie", false);
+        invoiceDao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(defaultInvoicePayment), context);
 
         final InvoiceModelDao retrievedInvoice1 = invoiceDao.getById(invoice.getId(), context);
         assertEquals(retrievedInvoice1.getInvoicePayments().size(), 1);
         assertEquals(retrievedInvoice1.getInvoicePayments().get(0).getSuccess(), Boolean.FALSE);
 
-        final DefaultInvoicePayment defaultInvoicePayment2 = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), clock.getUTCNow().plusDays(12), BigDecimal.TEN, Currency.USD, Currency.USD, true);
-        invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment2), context);
+        final DefaultInvoicePayment defaultInvoicePayment2 = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), clock.getUTCNow().plusDays(12), BigDecimal.TEN, Currency.USD, Currency.USD, "cookie", true);
+        invoiceDao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(defaultInvoicePayment2), context);
 
         final InvoiceModelDao retrievedInvoice2 = invoiceDao.getById(invoice.getId(), context);
         assertEquals(retrievedInvoice2.getInvoicePayments().size(), 1);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceItemDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceItemDao.java
index 99e777e..20c682e 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceItemDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceItemDao.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2010-2014 Ning, Inc.
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -193,9 +195,8 @@ public class TestInvoiceItemDao extends InvoiceTestSuiteWithEmbeddedDB {
 
     @Test(groups = "slow")
     public void testExternalChargeForVariousCurrenciesInvoiceSqlDao() throws Exception {
-        // 1 decimal place
+        // 0 decimal place
         createAndVerifyExternalCharge(new BigDecimal("10"), Currency.VND);
-        createAndVerifyExternalCharge(new BigDecimal("10.1"), Currency.VND);
         // 2 decimal places
         createAndVerifyExternalCharge(new BigDecimal("10"), Currency.USD);
         createAndVerifyExternalCharge(new BigDecimal("10.1"), Currency.USD);
@@ -215,6 +216,8 @@ public class TestInvoiceItemDao extends InvoiceTestSuiteWithEmbeddedDB {
         createAndVerifyExternalCharge(new BigDecimal("10.000001"), Currency.BTC);
         createAndVerifyExternalCharge(new BigDecimal("10.0000001"), Currency.BTC);
         createAndVerifyExternalCharge(new BigDecimal("10.00000001"), Currency.BTC);
+        // Malagasy ariary is subdivided into 5 iraimbilanja
+        createAndVerifyExternalCharge(new BigDecimal("10.2"), Currency.MGA);
     }
 
     private void createAndVerifyExternalCharge(final BigDecimal amount, final Currency currency) throws EntityPersistenceException {
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index 3d20246..5a6690f 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -919,7 +919,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
         // pay the invoice
         invoice1.addPayment(new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), april25.toDateTimeAtCurrentTime(), TEN,
-                                                      Currency.USD, Currency.USD, true));
+                                                      Currency.USD, Currency.USD, null, true));
         assertEquals(invoice1.getBalance().compareTo(ZERO), 0);
 
         // change the plan (i.e. repair) on start date
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
index 706d61b..a42f7de 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
@@ -228,7 +228,7 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
         fixedAndRecurringInvoiceItemGenerator.processFixedBillingEvents(invoiceId, account.getId(), events, targetDate, Currency.USD, proposedItems);
         assertEquals(proposedItems.size(), 1);
         assertEquals(proposedItems.get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
-        assertEquals(proposedItems.get(0).getAmount(), fixedPriceAmount);
+        assertEquals(proposedItems.get(0).getAmount().compareTo(fixedPriceAmount), 0);
     }
 
 
@@ -283,7 +283,7 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
         fixedAndRecurringInvoiceItemGenerator.processFixedBillingEvents(invoiceId, account.getId(), events, targetDate, Currency.USD, proposedItems);
         assertEquals(proposedItems.size(), 1);
         assertEquals(proposedItems.get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
-        assertEquals(proposedItems.get(0).getAmount(), fixedPriceAmount3);
+        assertEquals(proposedItems.get(0).getAmount().compareTo(fixedPriceAmount3), 0);
     }
 
 }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/model/TestExternalChargeInvoiceItem.java b/invoice/src/test/java/org/killbill/billing/invoice/model/TestExternalChargeInvoiceItem.java
index 35f50a9..67ff4f8 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/model/TestExternalChargeInvoiceItem.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/model/TestExternalChargeInvoiceItem.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2010-2014 Ning, Inc.
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -41,7 +43,7 @@ public class TestExternalChargeInvoiceItem extends InvoiceTestSuiteNoDB {
         final ExternalChargeInvoiceItem item = new ExternalChargeInvoiceItem(id, invoiceId, accountId, bundleId, description,
                                                                              effectiveDate, amount, currency);
         Assert.assertEquals(item.getAccountId(), accountId);
-        Assert.assertEquals(item.getAmount(), amount);
+        Assert.assertEquals(item.getAmount().compareTo(amount), 0);
         Assert.assertEquals(item.getBundleId(), bundleId);
         Assert.assertEquals(item.getCurrency(), currency);
         Assert.assertEquals(item.getInvoiceItemType(), InvoiceItemType.EXTERNAL_CHARGE);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java
index 9f9a536..f64b7ea 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java
@@ -135,7 +135,8 @@ public class InvoiceTestUtils {
         Mockito.when(payment.getProcessedCurrency()).thenReturn(currency);
         Mockito.when(payment.isSuccess()).thenReturn(true);
 
-        invoicePaymentApi.notifyOfPayment(payment, callContext);
+        invoicePaymentApi.recordPaymentAttemptCompletion(payment.getInvoiceId(), payment.getAmount(), payment.getCurrency(), payment.getProcessedCurrency(), payment.getPaymentId(), payment.getPaymentCookieId(),
+                                                         payment.getPaymentDate(), payment.isSuccess(), callContext);
 
         return payment;
     }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
index 725d6d7..d55669e 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
@@ -136,9 +136,9 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
         invoice.addInvoiceItem(creditBalanceAdjInvoiceItem2);
         invoice.addInvoiceItem(refundAdjInvoiceItem);
         invoice.addPayment(new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice.getId(), clock.getUTCNow(), BigDecimal.TEN,
-                                                     Currency.USD, Currency.USD, true));
+                                                     Currency.USD, Currency.USD, null, true));
         invoice.addPayment(new DefaultInvoicePayment(InvoicePaymentType.REFUND, UUID.randomUUID(), invoice.getId(), clock.getUTCNow(), BigDecimal.ONE.negate(),
-                                                     Currency.USD, Currency.USD, true));
+                                                     Currency.USD, Currency.USD, null, true));
         // Check the scenario
         Assert.assertEquals(invoice.getBalance().doubleValue(), 0.00);
         Assert.assertEquals(invoice.getCreditedAmount().doubleValue(), 11.00);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
index 0695857..f5eeeaa 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.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
@@ -152,7 +152,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         Assert.assertEquals(invoiceItems.get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
         Assert.assertEquals(invoiceItems.get(0).getStartDate(), new LocalDate("2012-05-01"));
         Assert.assertNull(invoiceItems.get(0).getEndDate());
-        Assert.assertEquals(invoiceItems.get(0).getAmount(), BigDecimal.ZERO);
+        Assert.assertEquals(invoiceItems.get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
         Assert.assertNull(invoiceItems.get(0).getRate());
 
         Assert.assertEquals(invoiceItems.get(1).getInvoiceItemType(), InvoiceItemType.RECURRING);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
index f9a86d1..102f9c7 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2014 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
@@ -34,7 +34,6 @@ import org.killbill.billing.invoice.model.ItemAdjInvoiceItem;
 import org.killbill.billing.invoice.model.RecurringInvoiceItem;
 import org.killbill.billing.invoice.model.RepairAdjInvoiceItem;
 import org.killbill.billing.util.jackson.ObjectMapper;
-import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import com.google.common.collect.Lists;
@@ -826,8 +825,7 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
     }
 
     @Test(groups = "fast")
-    public void verifyJson() {
-
+    public void verifyJson() throws IOException {
         final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
         final UUID id1 = UUID.fromString("e8ba6ce7-9bd4-417d-af53-70951ecaa99f");
         final InvoiceItem yearly1 = new RecurringInvoiceItem(id1, new DateTime(), invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, new LocalDate("2014-01-01"), new LocalDate("2015-01-01"), BigDecimal.TEN, BigDecimal.TEN, currency);
@@ -842,17 +840,12 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
         tree.addItem(repair);
 
         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        try {
-            tree.getRoot().jsonSerializeTree(new ObjectMapper(), outputStream);
-
-            final String json = outputStream.toString("UTF-8");
-            final String expectedJson = "[{\"start\":\"2014-01-01\",\"end\":\"2015-01-01\",\"items\":[{\"id\":\"e8ba6ce7-9bd4-417d-af53-70951ecaa99f\",\"startDate\":\"2014-01-01\",\"endDate\":\"2015-01-01\",\"amount\":10,\"currency\":\"USD\",\"linkedId\":null,\"action\":\"ADD\"}]},[{\"start\":\"2014-08-01\",\"end\":\"2015-01-01\",\"items\":[{\"id\":\"48db1317-9a6e-4666-bcc5-fc7d3d0defc8\",\"startDate\":\"2014-08-01\",\"endDate\":\"2015-01-01\",\"amount\":1,\"currency\":\"USD\",\"linkedId\":null,\"action\":\"ADD\"},{\"id\":\"02ec57f5-2723-478b-86ba-ebeaedacb9db\",\"startDate\":\"2014-08-01\",\"endDate\":\"2015-01-01\",\"amount\":10,\"currency\":\"USD\",\"linkedId\":\"e8ba6ce7-9bd4-417d-af53-70951ecaa99f\",\"action\":\"CANCEL\"}]}]]";
+        tree.getRoot().jsonSerializeTree(new ObjectMapper(), outputStream);
 
-            assertEquals(json, expectedJson);
+        final String json = outputStream.toString("UTF-8");
+        final String expectedJson = "[{\"start\":\"2014-01-01\",\"end\":\"2015-01-01\",\"items\":[{\"id\":\"e8ba6ce7-9bd4-417d-af53-70951ecaa99f\",\"startDate\":\"2014-01-01\",\"endDate\":\"2015-01-01\",\"amount\":10.00,\"currency\":\"USD\",\"linkedId\":null,\"action\":\"ADD\"}]},[{\"start\":\"2014-08-01\",\"end\":\"2015-01-01\",\"items\":[{\"id\":\"48db1317-9a6e-4666-bcc5-fc7d3d0defc8\",\"startDate\":\"2014-08-01\",\"endDate\":\"2015-01-01\",\"amount\":1.00,\"currency\":\"USD\",\"linkedId\":null,\"action\":\"ADD\"},{\"id\":\"02ec57f5-2723-478b-86ba-ebeaedacb9db\",\"startDate\":\"2014-08-01\",\"endDate\":\"2015-01-01\",\"amount\":10.00,\"currency\":\"USD\",\"linkedId\":\"e8ba6ce7-9bd4-417d-af53-70951ecaa99f\",\"action\":\"CANCEL\"}]}]]";
 
-        } catch (final IOException e) {
-            Assert.fail(e.getMessage());
-        }
+        assertEquals(json, expectedJson);
     }
 
     @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/286")
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java
index dd025c0..b7436b8 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -112,7 +112,7 @@ public class TestContiguousIntervalConsumableInArrear extends TestUsageInArrearB
         existingUsage.add(ii5);
 
         final BigDecimal result = intervalConsumableInArrear.computeBilledUsage(intervalConsumableInArrear.getBilledItems(startDate, endDate, existingUsage));
-        assertEquals(result, BigDecimal.TEN.add(BigDecimal.TEN));
+        assertEquals(result.compareTo(BigDecimal.TEN.add(BigDecimal.TEN)), 0);
     }
 
     @Test(groups = "fast")
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/ExceptionMapperBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/ExceptionMapperBase.java
index 8794159..35cd19f 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/ExceptionMapperBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/ExceptionMapperBase.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2010-2014 Ning, Inc.
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -112,55 +114,40 @@ public abstract class ExceptionMapperBase {
     }
 
     protected Response buildConflictingRequestResponse(final Exception e, final UriInfo uriInfo) {
-        // Log the full stacktrace
-        log.warn("Conflicting request", e);
-
         final Response.ResponseBuilder responseBuilder = Response.status(Status.CONFLICT);
         serializeException(e, uriInfo, responseBuilder);
-        return responseBuilder.build();
+        return new LoggingResponse(e, responseBuilder.build());
     }
 
     protected Response buildNotFoundResponse(final Exception e, final UriInfo uriInfo) {
-        // Log the full stacktrace
-        log.info("Not found", e);
-
         final Response.ResponseBuilder responseBuilder = Response.status(Status.NOT_FOUND);
         serializeException(e, uriInfo, responseBuilder);
-        return responseBuilder.build();
+        return new LoggingResponse(e, responseBuilder.build());
     }
 
     protected Response buildBadRequestResponse(final Exception e, final UriInfo uriInfo) {
-        // Log the full stacktrace
-        log.warn("Bad request", e);
-
         final Response.ResponseBuilder responseBuilder = Response.status(Status.BAD_REQUEST);
         serializeException(e, uriInfo, responseBuilder);
-        return responseBuilder.build();
+        return new LoggingResponse(e, responseBuilder.build());
     }
 
     protected Response buildAuthorizationErrorResponse(final Exception e, final UriInfo uriInfo) {
-        // Log the full stacktrace
-        log.warn("Authorization error", e);
-
         // TODO Forbidden?
         final Response.ResponseBuilder responseBuilder = Response.status(Status.UNAUTHORIZED);
         serializeException(e, uriInfo, responseBuilder);
-        return responseBuilder.build();
+        return new LoggingResponse(e, responseBuilder.build());
     }
 
     protected Response buildInternalErrorResponse(final Exception e, final UriInfo uriInfo) {
-        // Log the full stacktrace
-        log.warn("Internal error", e);
-
         final Response.ResponseBuilder responseBuilder = Response.status(Status.INTERNAL_SERVER_ERROR);
         serializeException(e, uriInfo, responseBuilder);
-        return responseBuilder.build();
+        return new LoggingResponse(e, responseBuilder.build());
     }
 
     protected Response buildPluginTimeoutResponse(final Exception e, final UriInfo uriInfo) {
         final Response.ResponseBuilder responseBuilder = Response.status(Status.ACCEPTED);
         serializeException(e, uriInfo, responseBuilder);
-        return responseBuilder.build();
+        return new LoggingResponse(e, responseBuilder.build());
     }
 
     private void serializeException(final Exception e, final UriInfo uriInfo, final Response.ResponseBuilder responseBuilder) {
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/LoggingResponse.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/LoggingResponse.java
new file mode 100644
index 0000000..e3635ca
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/LoggingResponse.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LoggingResponse extends Response {
+
+    private static final Logger log = LoggerFactory.getLogger(LoggingResponse.class);
+
+    private final Exception e;
+    private final Response response;
+
+    public LoggingResponse(final Exception e, final Response response) {
+        this.e = e;
+        this.response = response;
+    }
+
+    @Override
+    public Object getEntity() {
+        // Delay logging until the entity is retrieved: this is to avoid double logging with TimedResourceInterceptor
+        // which needs to access exception mappers to get the response status
+        if (response.getStatus() == Status.CONFLICT.getStatusCode()) {
+            log.warn("Conflicting request", e);
+        } else if (response.getStatus() == Status.NOT_FOUND.getStatusCode()) {
+            log.debug("Not found", e);
+        } else if (response.getStatus() == Status.BAD_REQUEST.getStatusCode()) {
+            log.warn("Bad request", e);
+        } else if (response.getStatus() == Status.UNAUTHORIZED.getStatusCode()) {
+            log.debug("Authorization error", e);
+        } else if (response.getStatus() == Status.INTERNAL_SERVER_ERROR.getStatusCode()) {
+            log.warn("Internal error", e);
+        }
+
+        return response.getEntity();
+    }
+
+    @Override
+    public int getStatus() {
+        return response.getStatus();
+    }
+
+    @Override
+    public MultivaluedMap<String, Object> getMetadata() {
+        return response.getMetadata();
+    }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/PaymentApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/PaymentApiExceptionMapper.java
index c89369d..efa1828 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/PaymentApiExceptionMapper.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/PaymentApiExceptionMapper.java
@@ -69,7 +69,7 @@ public class PaymentApiExceptionMapper extends ExceptionMapperBase implements Ex
         } else if (exception.getCode() == ErrorCode.PAYMENT_UPD_PAYMENT_METHOD.getCode()) {
             return buildInternalErrorResponse(exception, uriInfo);
         } else if (exception.getCode() == ErrorCode.PAYMENT_INVALID_PARAMETER.getCode()) {
-            return buildInternalErrorResponse(exception, uriInfo);
+            return buildBadRequestResponse(exception, uriInfo);
         } else {
             return fallback(exception, uriInfo);
         }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
index 3a83f47..e0c27ce 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
@@ -92,6 +92,7 @@ import org.killbill.billing.payment.api.PaymentApi;
 import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.api.PaymentMethod;
 import org.killbill.billing.payment.api.PaymentOptions;
+import org.killbill.billing.payment.api.PaymentTransaction;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.util.UUIDs;
@@ -115,6 +116,7 @@ import org.killbill.commons.metrics.MetricTag;
 import org.killbill.commons.metrics.TimedResource;
 
 import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
@@ -915,12 +917,28 @@ public class AccountResource extends JaxRsResourceBase {
                              json.getAmount(), "PaymentTransactionJson amount needs to be set");
 
         final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
-        final UUID paymentMethodId = paymentMethodIdStr == null ? account.getPaymentMethodId() : UUID.fromString(paymentMethodIdStr);
         final Currency currency = json.getCurrency() == null ? account.getCurrency() : Currency.valueOf(json.getCurrency());
         final UUID paymentId = json.getPaymentId() == null ? null : UUID.fromString(json.getPaymentId());
 
+        //
+        // If paymentId was specified, it means we are attempting a payment completion. The preferred way is to use the PaymentResource
+        // (PUT /1.0/kb/payments/{paymentId}/completeTransaction), but for backward compatibility we still allow the call to proceed
+        // as long as the request/existing state is healthy (i.e there is a matching PENDING transaction)
+        //
+        final UUID paymentMethodId;
+        if (paymentId != null) {
+            final Payment initialPayment = paymentApi.getPayment(paymentId, false, pluginProperties, callContext);
+            final PaymentTransaction pendingTransaction = lookupPendingTransaction(initialPayment,
+                                                                                   json != null ? json.getTransactionId() : null,
+                                                                                   json != null ? json.getTransactionExternalKey() : null,
+                                                                                   json != null ? json.getTransactionType() : null);
+            paymentMethodId = initialPayment.getPaymentMethodId();
+        } else {
+            paymentMethodId = paymentMethodIdStr == null ? account.getPaymentMethodId() : UUID.fromString(paymentMethodIdStr);
+        }
         validatePaymentMethodForAccount(account.getId(), paymentMethodId, callContext);
 
+
         final TransactionType transactionType = TransactionType.valueOf(json.getTransactionType());
         final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
         final Payment result;
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoicePaymentResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoicePaymentResource.java
index 9ee85d0..a7b2c9a 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoicePaymentResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoicePaymentResource.java
@@ -125,7 +125,7 @@ public class InvoicePaymentResource extends JaxRsResourceBase {
         final InvoicePayment invoicePayment = Iterables.tryFind(invoicePayments, new Predicate<InvoicePayment>() {
             @Override
             public boolean apply(final InvoicePayment input) {
-                return input.getType() == InvoicePaymentType.ATTEMPT;
+                return input.getType() == InvoicePaymentType.ATTEMPT && input.isSuccess();
             }
         }).orNull();
         final UUID invoiceId = invoicePayment != null ? invoicePayment.getInvoiceId() : null;
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
index 845bfd0..dcb2738 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
@@ -611,9 +611,11 @@ public class InvoiceResource extends JaxRsResourceBase {
                                      (payment.getPaymentMethodId() != null ? UUID.fromString(payment.getPaymentMethodId()) : account.getPaymentMethodId());
 
         final UUID invoiceId = UUID.fromString(payment.getTargetInvoiceId());
+
         final Payment result = createPurchaseForInvoice(account, invoiceId, payment.getPurchasedAmount(), paymentMethodId, externalPayment, pluginProperties, callContext);
-        // STEPH should that live in InvoicePayment instead?
-        return uriBuilder.buildResponse(uriInfo, InvoicePaymentResource.class, "getInvoicePayment", result.getId());
+        return result != null ?
+               uriBuilder.buildResponse(uriInfo, InvoicePaymentResource.class, "getInvoicePayment", result.getId()) :
+               Response.status(Status.NO_CONTENT).build();
     }
 
     @TimedResource
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
index 6292294..ca71075 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -65,6 +65,7 @@ import org.killbill.billing.payment.api.PaymentMethod;
 import org.killbill.billing.payment.api.PaymentOptions;
 import org.killbill.billing.payment.api.PaymentTransaction;
 import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.api.TransactionStatus;
 import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.util.UUIDs;
 import org.killbill.billing.util.api.AuditUserApi;
@@ -298,6 +299,56 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
         }
     }
 
+    protected PaymentTransaction lookupPendingTransaction(final Payment initialPayment, @Nullable final String transactionId, @Nullable final String transactionExternalKey, @Nullable final String transactionType) throws PaymentApiException {
+        final Collection<PaymentTransaction> pendingTransaction  =  Collections2.filter(initialPayment.getTransactions(), new Predicate<PaymentTransaction>() {
+            @Override
+            public boolean apply(final PaymentTransaction input) {
+                if (input.getTransactionStatus() != TransactionStatus.PENDING) {
+                    return false;
+                }
+                if (transactionId != null && !transactionId.equals(input.getId().toString())) {
+                    return false;
+                }
+                if (transactionExternalKey != null && !transactionExternalKey.equals(input.getExternalKey())) {
+                    return false;
+                }
+                if (transactionType != null && !transactionType.equals(input.getTransactionType().name())) {
+                    return false;
+                }
+                //
+                // If we were given a transactionId or a transactionExternalKey or a transactionType we checked there was a match;
+                // In the worst case, if we were given nothing, we return the PENDING transaction for that payment
+                //
+                return true;
+            }
+        });
+        switch (pendingTransaction.size()) {
+            // Nothing: invalid input...
+            case 0:
+                final String parameterType;
+                final String parameterValue;
+                if (transactionId != null) {
+                    parameterType = "transactionId";
+                    parameterValue = transactionId;
+                } else if (transactionExternalKey != null) {
+                    parameterType = "transactionExternalKey";
+                    parameterValue = transactionExternalKey;
+                } else if (transactionType != null) {
+                    parameterType = "transactionType";
+                    parameterValue = transactionType;
+                } else {
+                    parameterType = "paymentId";
+                    parameterValue = initialPayment.getId().toString();
+                }
+                throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, parameterType, parameterValue);
+            case 1:
+                return pendingTransaction.iterator().next();
+            default:
+                throw new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, String.format("Illegal payment state: Found multiple PENDING payment transactions for paymentId='%s'", initialPayment.getId()));
+
+        }
+    }
+
     protected LocalDate toLocalDate(final UUID accountId, final String inputDate, final TenantContext context) throws AccountApiException {
         final LocalDate maybeResult = extractLocalDate(inputDate);
         if (maybeResult != null) {
@@ -399,9 +450,16 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
         final PluginProperty invoiceProperty = new PluginProperty("IPCD_INVOICE_ID" /* InvoicePaymentControlPluginApi.PROP_IPCD_INVOICE_ID (contract with plugin)  */,
                                                                   invoiceId.toString(), false);
         properties.add(invoiceProperty);
-
-        return paymentApi.createPurchaseWithPaymentControl(account, paymentMethodId, null, amountToPay, account.getCurrency(), paymentExternalKey, transactionExternalKey,
-                                                           properties, createInvoicePaymentControlPluginApiPaymentOptions(externalPayment), callContext);
+        try {
+            return paymentApi.createPurchaseWithPaymentControl(account, paymentMethodId, null, amountToPay, account.getCurrency(), paymentExternalKey, transactionExternalKey,
+                                                               properties, createInvoicePaymentControlPluginApiPaymentOptions(externalPayment), callContext);
+        } catch (final PaymentApiException e) {
+            if (e.getCode() == ErrorCode.PAYMENT_PLUGIN_EXCEPTION.getCode() &&
+                e.getMessage().contains("Aborted Payment for invoice")) {
+                return null;
+            }
+            throw e;
+        }
     }
 
     protected PaymentOptions createInvoicePaymentControlPluginApiPaymentOptions(final boolean isExternalPayment) {
@@ -445,7 +503,7 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
         final InvoicePayment invoicePayment = Iterables.tryFind(invoicePayments, new Predicate<InvoicePayment>() {
             @Override
             public boolean apply(final InvoicePayment input) {
-                return input.getPaymentId().equals(payment.getId()) && input.getType() == InvoicePaymentType.ATTEMPT;
+                return input.isSuccess() && input.getPaymentId().equals(payment.getId()) && input.getType() == InvoicePaymentType.ATTEMPT;
             }
         }).orNull();
         return invoicePayment != null ? invoicePayment.getInvoiceId() : null;
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentMethodResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentMethodResource.java
index 07fdd5e..731db34 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentMethodResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentMethodResource.java
@@ -179,7 +179,7 @@ public class PaymentMethodResource extends JaxRsResourceBase {
                                                                 account = accountUserApi.getAccountById(paymentMethod.getAccountId(), tenantContext);
                                                                 accounts.put(paymentMethod.getAccountId(), account);
                                                             } catch (final AccountApiException e) {
-                                                                log.warn("Unable to retrieve account", e);
+                                                                log.warn("Error retrieving accountId='{}'", paymentMethod.getAccountId(), e);
                                                                 return null;
                                                             }
                                                         }
@@ -238,7 +238,7 @@ public class PaymentMethodResource extends JaxRsResourceBase {
                                                                 account = accountUserApi.getAccountById(paymentMethod.getAccountId(), tenantContext);
                                                                 accounts.put(paymentMethod.getAccountId(), account);
                                                             } catch (final AccountApiException e) {
-                                                                log.warn("Unable to retrieve account", e);
+                                                                log.warn("Error retrieving accountId='{}'", paymentMethod.getAccountId(), e);
                                                                 return null;
                                                             }
                                                         }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
index e661f7a..2463abe 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
@@ -19,7 +19,6 @@ package org.killbill.billing.jaxrs.resources;
 
 import java.math.BigDecimal;
 import java.net.URI;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -73,9 +72,7 @@ import org.killbill.commons.metrics.MetricTag;
 import org.killbill.commons.metrics.TimedResource;
 
 import com.google.common.base.Function;
-import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
-import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableMap;
 import com.wordnik.swagger.annotations.Api;
 import com.wordnik.swagger.annotations.ApiOperation;
@@ -266,6 +263,11 @@ public class PaymentResource extends ComboPaymentResource {
         return completeTransactionInternal(json, null, paymentControlPluginNames, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
     }
 
+
+
+
+
+
     private Response completeTransactionInternal(final PaymentTransactionJson json,
                                                  @Nullable final String paymentIdStr,
                                                  final List<String> paymentControlPluginNames,
@@ -275,6 +277,7 @@ public class PaymentResource extends ComboPaymentResource {
                                                  final String comment,
                                                  final UriInfo uriInfo,
                                                  final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+
         final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
         final CallContext callContext = context.createContext(createdBy, reason, comment, request);
         final Payment initialPayment = getPaymentByIdOrKey(paymentIdStr, json == null ? null : json.getPaymentExternalKey(), pluginProperties, callContext);
@@ -283,96 +286,38 @@ public class PaymentResource extends ComboPaymentResource {
         final BigDecimal amount = json == null ? null : json.getAmount();
         final Currency currency = json == null || json.getCurrency() == null ? null : Currency.valueOf(json.getCurrency());
 
-        final TransactionType transactionType;
-        final String transactionExternalKey;
-        if (json != null && json.getTransactionId() != null) {
-            final Collection<PaymentTransaction> paymentTransactionCandidates = Collections2.<PaymentTransaction>filter(initialPayment.getTransactions(),
-                                                                                                                        new Predicate<PaymentTransaction>() {
-                                                                                                                            @Override
-                                                                                                                            public boolean apply(final PaymentTransaction input) {
-                                                                                                                                return input.getId().toString().equals(json.getTransactionId());
-                                                                                                                            }
-                                                                                                                        });
-            if (paymentTransactionCandidates.size() == 1) {
-                final PaymentTransaction paymentTransaction = paymentTransactionCandidates.iterator().next();
-                transactionType = paymentTransaction.getTransactionType();
-                transactionExternalKey = paymentTransaction.getExternalKey();
-            } else {
-                return Response.status(Status.NOT_FOUND).build();
-            }
-        } else if (json != null && json.getTransactionExternalKey() != null && json.getTransactionType() != null) {
-            transactionType = TransactionType.valueOf(json.getTransactionType());
-            transactionExternalKey = json.getTransactionExternalKey();
-        } else if (json != null && json.getTransactionExternalKey() != null) {
-            final Collection<PaymentTransaction> paymentTransactionCandidates = Collections2.<PaymentTransaction>filter(initialPayment.getTransactions(),
-                                                                                                                        new Predicate<PaymentTransaction>() {
-                                                                                                                            @Override
-                                                                                                                            public boolean apply(final PaymentTransaction input) {
-                                                                                                                                return input.getExternalKey().equals(json.getTransactionExternalKey());
-                                                                                                                            }
-                                                                                                                        });
-            if (paymentTransactionCandidates.size() == 1) {
-                transactionType = paymentTransactionCandidates.iterator().next().getTransactionType();
-                transactionExternalKey = json.getTransactionExternalKey();
-            } else {
-                // Note: we could bit a bit smarter but keep the logic in the payment system
-                verifyNonNullOrEmpty(null, "PaymentTransactionJson transactionType needs to be set");
-                // Never reached
-                return Response.status(Status.PRECONDITION_FAILED).build();
-            }
-        } else if (json != null && json.getTransactionType() != null) {
-            final Collection<PaymentTransaction> paymentTransactionCandidates = Collections2.<PaymentTransaction>filter(initialPayment.getTransactions(),
-                                                                                                                        new Predicate<PaymentTransaction>() {
-                                                                                                                            @Override
-                                                                                                                            public boolean apply(final PaymentTransaction input) {
-                                                                                                                                return input.getTransactionType().toString().equals(json.getTransactionType());
-                                                                                                                            }
-                                                                                                                        });
-            if (paymentTransactionCandidates.size() == 1) {
-                transactionType = TransactionType.valueOf(json.getTransactionType());
-                transactionExternalKey = paymentTransactionCandidates.iterator().next().getExternalKey();
-            } else {
-                verifyNonNullOrEmpty(null, "PaymentTransactionJson externalKey needs to be set");
-                // Never reached
-                return Response.status(Status.PRECONDITION_FAILED).build();
-            }
-        } else if (initialPayment.getTransactions().size() == 1) {
-            final PaymentTransaction paymentTransaction = initialPayment.getTransactions().get(0);
-            transactionType = paymentTransaction.getTransactionType();
-            transactionExternalKey = paymentTransaction.getExternalKey();
-        } else {
-            verifyNonNullOrEmpty(null, "PaymentTransactionJson transactionType and externalKey need to be set");
-            // Never reached
-            return Response.status(Status.PRECONDITION_FAILED).build();
-        }
+            final PaymentTransaction pendingTransaction = lookupPendingTransaction(initialPayment,
+                                                                                   json != null ? json.getTransactionId() : null,
+                                                                                   json != null ? json.getTransactionExternalKey() : null,
+                                                                                   json != null ? json.getTransactionType() : null);
 
-        final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
-        switch (transactionType) {
+            final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
+            switch (pendingTransaction.getTransactionType()) {
             case AUTHORIZE:
                 paymentApi.createAuthorizationWithPaymentControl(account, initialPayment.getPaymentMethodId(), initialPayment.getId(), amount, currency,
-                                                                 initialPayment.getExternalKey(), transactionExternalKey,
+                                                                 initialPayment.getExternalKey(), pendingTransaction.getExternalKey(),
                                                                  pluginProperties, paymentOptions, callContext);
                 break;
             case CAPTURE:
-                paymentApi.createCaptureWithPaymentControl(account, initialPayment.getId(), amount, currency, transactionExternalKey,
+                paymentApi.createCaptureWithPaymentControl(account, initialPayment.getId(), amount, currency, pendingTransaction.getExternalKey(),
                                                            pluginProperties, paymentOptions, callContext);
                 break;
             case PURCHASE:
                 paymentApi.createPurchaseWithPaymentControl(account, initialPayment.getPaymentMethodId(), initialPayment.getId(), amount, currency,
-                                                            initialPayment.getExternalKey(), transactionExternalKey,
+                                                            initialPayment.getExternalKey(), pendingTransaction.getExternalKey(),
                                                             pluginProperties, paymentOptions, callContext);
                 break;
             case CREDIT:
                 paymentApi.createCreditWithPaymentControl(account, initialPayment.getPaymentMethodId(), initialPayment.getId(), amount, currency,
-                                                          initialPayment.getExternalKey(), transactionExternalKey,
+                                                          initialPayment.getExternalKey(), pendingTransaction.getExternalKey(),
                                                           pluginProperties, paymentOptions, callContext);
                 break;
             case REFUND:
                 paymentApi.createRefundWithPaymentControl(account, initialPayment.getId(), amount, currency,
-                                                          transactionExternalKey, pluginProperties, paymentOptions, callContext);
+                                                          pendingTransaction.getExternalKey(), pluginProperties, paymentOptions, callContext);
                 break;
             default:
-                return Response.status(Status.PRECONDITION_FAILED).entity("TransactionType " + transactionType + " cannot be completed").build();
+                return Response.status(Status.PRECONDITION_FAILED).entity("TransactionType " + pendingTransaction.getTransactionType() + " cannot be completed").build();
         }
         return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", initialPayment.getId());
     }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
index 7749b79..62ddc79 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
@@ -92,7 +92,6 @@ import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
-import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 import com.wordnik.swagger.annotations.Api;
@@ -515,20 +514,19 @@ public class SubscriptionResource extends JaxRsResourceBase {
         @Override
         public void onSubscriptionBaseTransition(final EffectiveSubscriptionInternalEvent event) {
 
-            log.info(String.format("Got event SubscriptionBaseTransition token = %s, type = %s, remaining = %d ",
-                                   event.getUserToken(), event.getTransitionType(), event.getRemainingEventsForUserOperation()));
+            log.info("Got event SubscriptionBaseTransition token='{}', type='{}', remaining='{}'", event.getUserToken(), event.getTransitionType(), event.getRemainingEventsForUserOperation());
         }
 
         @Override
         public void onEmptyInvoice(final NullInvoiceInternalEvent event) {
-            log.info(String.format("Got event EmptyInvoiceNotification token = %s ", event.getUserToken()));
+            log.info("Got event EmptyInvoiceNotification token='{}'", event.getUserToken());
             notifyForCompletion();
         }
 
         @Override
         public void onInvoiceCreation(final InvoiceCreationInternalEvent event) {
 
-            log.info(String.format("Got event InvoiceCreationNotification token = %s ", event.getUserToken()));
+            log.info("Got event InvoiceCreationNotification token='{}'", event.getUserToken());
             if (event.getAmountOwed().compareTo(BigDecimal.ZERO) <= 0) {
                 notifyForCompletion();
             }
@@ -536,19 +534,19 @@ public class SubscriptionResource extends JaxRsResourceBase {
 
         @Override
         public void onPaymentInfo(final PaymentInfoInternalEvent event) {
-            log.info(String.format("Got event PaymentInfo token = %s ", event.getUserToken()));
+            log.info("Got event PaymentInfo token='{}'", event.getUserToken());
             notifyForCompletion();
         }
 
         @Override
         public void onPaymentError(final PaymentErrorInternalEvent event) {
-            log.info(String.format("Got event PaymentError token = %s ", event.getUserToken()));
+            log.info("Got event PaymentError token='{}'", event.getUserToken());
             notifyForCompletion();
         }
 
         @Override
         public void onPaymentPluginError(final PaymentPluginErrorInternalEvent event) {
-            log.info(String.format("Got event PaymentPluginError token = %s ", event.getUserToken()));
+            log.info("Got event PaymentPluginError token='{}'", event.getUserToken());
             notifyForCompletion();
         }
     }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TransactionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TransactionResource.java
index 322731d..f994dd7 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TransactionResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TransactionResource.java
@@ -58,7 +58,7 @@ import com.wordnik.swagger.annotations.ApiResponses;
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 
 @Path(JaxrsResource.PAYMENT_TRANSACTIONS_PATH)
-@Api(value = JaxrsResource.PAYMENT_TRANSACTIONS, description = "Operations on payment transactions")
+@Api(value = JaxrsResource.PAYMENT_TRANSACTIONS_PATH, description = "Operations on payment transactions")
 public class TransactionResource extends JaxRsResourceBase {
 
     @Inject
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/Context.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/Context.java
index b2a67c0..d431641 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/Context.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/Context.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2010-2014 Ning, Inc.
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -16,6 +18,8 @@
 
 package org.killbill.billing.jaxrs.util;
 
+import java.util.UUID;
+
 import javax.servlet.ServletRequest;
 
 import org.killbill.billing.jaxrs.resources.JaxrsResource;
@@ -24,8 +28,10 @@ import org.killbill.billing.util.UUIDs;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.CallContextFactory;
 import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.callcontext.TenantContext;
 import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.commons.request.Request;
 
 import com.google.common.base.Preconditions;
 import com.google.inject.Inject;
@@ -34,13 +40,15 @@ public class Context {
 
     private final CallOrigin origin;
     private final UserType userType;
-    final CallContextFactory contextFactory;
+    private final CallContextFactory contextFactory;
+    private final InternalCallContextFactory internalCallContextFactory;
 
     @Inject
-    public Context(final CallContextFactory factory) {
+    public Context(final CallContextFactory factory, final InternalCallContextFactory internalCallContextFactory) {
         this.origin = CallOrigin.EXTERNAL;
         this.userType = UserType.CUSTOMER;
         this.contextFactory = factory;
+        this.internalCallContextFactory = internalCallContextFactory;
     }
 
     public CallContext createContext(final String createdBy, final String reason, final String comment, final ServletRequest request)
@@ -48,21 +56,46 @@ public class Context {
         try {
             Preconditions.checkNotNull(createdBy, String.format("Header %s needs to be set", JaxrsResource.HDR_CREATED_BY));
             final Tenant tenant = getTenantFromRequest(request);
-            return contextFactory.createCallContext(tenant == null ? null : tenant.getId(), createdBy, origin, userType, reason,
-                                                    comment, UUIDs.randomUUID());
+            final CallContext callContext = contextFactory.createCallContext(tenant == null ? null : tenant.getId(), createdBy, origin, userType, reason,
+                                                                             comment, getOrCreateUserToken());
+
+            populateMDCContext(callContext);
+
+            return callContext;
         } catch (final NullPointerException e) {
             throw new IllegalArgumentException(e.getMessage());
         }
     }
 
     public TenantContext createContext(final ServletRequest request) {
+        final TenantContext tenantContext;
+
         final Tenant tenant = getTenantFromRequest(request);
         if (tenant == null) {
             // Multi-tenancy may not have been configured - default to "default" tenant (see InternalCallContextFactory)
-            return contextFactory.createTenantContext(null);
+            tenantContext = contextFactory.createTenantContext(null);
+        } else {
+            tenantContext = contextFactory.createTenantContext(tenant.getId());
+        }
+
+        populateMDCContext(tenantContext);
+
+        return tenantContext;
+    }
+
+    // Use REQUEST_ID_HEADER if this is provided and lloks like a UUID, if not allocate a random one.
+    private UUID getOrCreateUserToken() {
+        UUID userToken;
+        if (Request.getPerThreadRequestData().getRequestId() != null) {
+            try {
+                userToken = UUID.fromString(Request.getPerThreadRequestData().getRequestId());
+            } catch (final IllegalArgumentException ignored) {
+                userToken = UUIDs.randomUUID();
+            }
         } else {
-            return contextFactory.createTenantContext(tenant.getId());
+            userToken = UUIDs.randomUUID();
         }
+        return userToken;
     }
 
     private Tenant getTenantFromRequest(final ServletRequest request) {
@@ -74,4 +107,9 @@ public class Context {
             return (Tenant) tenantObject;
         }
     }
+
+    private void populateMDCContext(final TenantContext tenantContext) {
+        // InternalCallContextFactory will do it for us
+        internalCallContextFactory.createInternalTenantContext(tenantContext);
+    }
 }
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java
index 84b3bc1..733f876 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java
@@ -144,7 +144,7 @@ public class BillCycleDayCalculator {
 
         final DateTime date = plan.dateOfFirstRecurringNonZeroCharge(subscription.getStartDate(), initialPhaseType);
         final int bcdLocal = context.toDateTime(date, account.getTimeZone()).getDayOfMonth();
-        log.info("Calculated BCD: subscription id {}, subscription start {}, timezone {}, bcd {}",
+        log.info("Calculated BCD: subscriptionId='{}', subscriptionStartDate='{}', accountTimeZone='{}', bcd='{}'",
                  subscription.getId(), date.toDateTimeISO(), account.getTimeZone(), bcdLocal);
 
         return bcdLocal;
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
index 359975f..f263960 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
@@ -110,7 +110,7 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
         }
 
         // Pretty-print the events, before and after the blocking calculator does its magic
-        final StringBuilder logStringBuilder = new StringBuilder("Computed billing events for accountId ").append(accountId);
+        final StringBuilder logStringBuilder = new StringBuilder("Computed billing events for accountId='").append(accountId).append("'");
         eventsToString(logStringBuilder, result, "\nBilling Events Raw");
         blockCalculator.insertBlockingEvents(result, skippedSubscriptions, context);
         eventsToString(logStringBuilder, result, "\nBilling Events After Blocking");
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
index 91a0e03..22d0865 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
@@ -27,7 +27,6 @@ import java.util.UUID;
 import javax.inject.Named;
 
 import org.joda.time.DateTime;
-import org.joda.time.LocalDate;
 import org.joda.time.Period;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.ObjectType;
@@ -171,11 +170,20 @@ public class OverdueStateApplicator {
         } catch (final AccountApiException e) {
             throw new OverdueException(e);
         }
+
+        final OverdueChangeInternalEvent event;
+        try {
+            event = createOverdueEvent(account, previousOverdueState.getName(), nextOverdueState.getName(), isBlockBillingTransition(previousOverdueState, nextOverdueState),
+                                       isUnblockBillingTransition(previousOverdueState, nextOverdueState), context);
+        } catch (final BlockingApiException e) {
+            log.warn("Failed to create OverdueChangeInternalEvent for accountId='{}'", account.getId(), e);
+            return;
+        }
+
         try {
-            bus.post(createOverdueEvent(account, previousOverdueState.getName(), nextOverdueState.getName(), isBlockBillingTransition(previousOverdueState, nextOverdueState),
-                                        isUnblockBillingTransition(previousOverdueState, nextOverdueState), context));
+            bus.post(event);
         } catch (final Exception e) {
-            log.error("Error posting overdue change event to bus", e);
+            log.warn("Failed to post event {}", event, e);
         }
     }
 
@@ -218,11 +226,19 @@ public class OverdueStateApplicator {
             throw new OverdueException(e);
         }
 
+        final OverdueChangeInternalEvent event;
+        try {
+            event = createOverdueEvent(account, previousOverdueState.getName(), clearState.getName(), isBlockBillingTransition(previousOverdueState, clearState),
+                                       isUnblockBillingTransition(previousOverdueState, clearState), context);
+        } catch (final BlockingApiException e) {
+            log.warn("Failed to create OverdueChangeInternalEvent for accountId='{}'", account.getId(), e);
+            return;
+        }
+
         try {
-            bus.post(createOverdueEvent(account, previousOverdueState.getName(), clearState.getName(), isBlockBillingTransition(previousOverdueState, clearState),
-                                        isUnblockBillingTransition(previousOverdueState, clearState), context));
+            bus.post(event);
         } catch (final Exception e) {
-            log.error("Error posting overdue change event to bus", e);
+            log.warn("Failed to post event {}", event, e);
         }
     }
 
@@ -372,11 +388,11 @@ public class OverdueStateApplicator {
                 emailSender.sendPlainTextEmail(to, cc, subject, emailBody);
             }
         } catch (final IOException e) {
-            log.warn(String.format("Unable to generate or send overdue notification email for account %s and overdueable %s", account.getId(), account.getId()), e);
+            log.warn("Unable to generate or send overdue notification email for accountId='{}'", account.getId(), e);
         } catch (final EmailApiException e) {
-            log.warn(String.format("Unable to send overdue notification email for account %s and overdueable %s", account.getId(), account.getId()), e);
+            log.warn("Unable to send overdue notification email for accountId='{}'", account.getId(), e);
         } catch (final MustacheException e) {
-            log.warn(String.format("Unable to generate overdue notification email for account %s and overdueable %s", account.getId(), account.getId()), e);
+            log.warn("Unable to generate overdue notification email for accountId='{}'", account.getId(), e);
         }
     }
 
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/caching/EhCacheOverdueConfigCache.java b/overdue/src/main/java/org/killbill/billing/overdue/caching/EhCacheOverdueConfigCache.java
index d52470c..7285e87 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/caching/EhCacheOverdueConfigCache.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/caching/EhCacheOverdueConfigCache.java
@@ -60,7 +60,7 @@ public class EhCacheOverdueConfigCache implements OverdueConfigCache {
             defaultOverdueConfig = XMLLoader.getObjectFromUri(noOverdueConfigURI, DefaultOverdueConfig.class);
         } catch (final Exception e) {
             defaultOverdueConfig = new DefaultOverdueConfig();
-            log.warn("Exception loading NoOverdueConfig - should never happen!", e);
+            log.error("Exception loading NoOverdueConfig - should never happen!", e);
         }
     }
 
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/caching/OverdueCacheInvalidationCallback.java b/overdue/src/main/java/org/killbill/billing/overdue/caching/OverdueCacheInvalidationCallback.java
index e7fb85e..4a19678 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/caching/OverdueCacheInvalidationCallback.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/caching/OverdueCacheInvalidationCallback.java
@@ -38,7 +38,7 @@ public class OverdueCacheInvalidationCallback implements CacheInvalidationCallba
 
     @Override
     public void invalidateCache(TenantKey key, final Object cookie, final InternalTenantContext tenantContext) {
-        log.info("Invalidate overdue cache for tenant {} ", tenantContext.getTenantRecordId());
+        log.info("Invalidate overdue cache for tenantRecordId='{}'", tenantContext.getTenantRecordId());
         overdueConfigCache.clearOverdueConfig(tenantContext);
     }
 }
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueDispatcher.java b/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueDispatcher.java
index 58653d2..c631b72 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueDispatcher.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueDispatcher.java
@@ -50,7 +50,7 @@ public class OverdueDispatcher {
         try {
             factory.createOverdueWrapperFor(accountId, context).refresh(context);
         } catch (BillingExceptionBase e) {
-            log.error(String.format("Error processing Overdue for blockable %s", accountId), e);
+            log.warn("Error processing Overdue for accountId='{}'", accountId, e);
         }
     }
 
@@ -58,7 +58,7 @@ public class OverdueDispatcher {
         try {
             factory.createOverdueWrapperFor(accountId, context).clear(context);
         } catch (BillingExceptionBase e) {
-            log.error(String.format("Error processing Overdue for blockable %s (type %s)", accountId), e);
+            log.warn("Error processing Overdue for accountId='{}'", accountId, e);
         }
     }
 }
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/service/DefaultOverdueService.java b/overdue/src/main/java/org/killbill/billing/overdue/service/DefaultOverdueService.java
index 87cc174..7aa0bed 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/service/DefaultOverdueService.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/service/DefaultOverdueService.java
@@ -91,8 +91,7 @@ public class DefaultOverdueService implements OverdueService {
                 overdueConfigCache.loadDefaultOverdueConfig(properties.getConfigURI());
                 isConfigLoaded = true;
             } catch (OverdueApiException e) {
-                log.warn("Overdue system disabled: unable to loadDefaultCatalog the overdue config from " + properties.getConfigURI(), e);
-                e.printStackTrace();
+                log.warn("Overdue system disabled: unable to load the overdue config from uri='{}'", properties.getConfigURI(), e);
             }
         }
     }
@@ -109,7 +108,7 @@ public class DefaultOverdueService implements OverdueService {
         try {
             busService.getBus().register(listener);
         } catch (final EventBusException e) {
-            log.error("Problem encountered registering OverdueListener on the Event Bus", e);
+            log.error("Failed to register OverdueListener", e);
         }
     }
 
@@ -124,7 +123,7 @@ public class DefaultOverdueService implements OverdueService {
         try {
             busService.getBus().unregister(listener);
         } catch (final EventBusException e) {
-            log.error("Problem encountered registering OverdueListener on the Event Bus", e);
+            log.error("Failed to unregister OverdueListener", e);
         }
         checkNotifier.stop();
         asyncNotifier.stop();
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java
index 95a2b7d..7743e65 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java
@@ -87,8 +87,7 @@ public class OverdueWrapper {
 
             return refreshWithLock(context);
         } catch (final LockFailedException e) {
-            // Not good!
-            log.error(String.format("Failed to process overdue for account %s", overdueable.getId()), e);
+            log.warn("Failed to process overdue for accountId='{}'", overdueable.getId(), e);
         } finally {
             if (lock != null) {
                 lock.release();
@@ -116,8 +115,7 @@ public class OverdueWrapper {
 
             clearWithLock(context);
         } catch (final LockFailedException e) {
-            // Not good!
-            log.error(String.format("Failed to clear overdue for account %s", overdueable.getId()), e);
+            log.warn("Failed to clear overdue for accountId='{}'", overdueable.getId(), e);
         } finally {
             if (lock != null) {
                 lock.release();
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultApiBase.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultApiBase.java
index cd916d9..74cc2d1 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultApiBase.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultApiBase.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -32,11 +32,13 @@ import org.killbill.billing.util.config.PaymentConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 
 public class DefaultApiBase {
 
     private static final Logger log = LoggerFactory.getLogger(DefaultApiBase.class);
+    private static final Joiner JOINER = Joiner.on(",");
 
     private final PaymentConfig paymentConfig;
 
@@ -44,40 +46,68 @@ public class DefaultApiBase {
         this.paymentConfig = paymentConfig;
     }
 
-    protected void logAPICall(final String transactionType, final Account account, final UUID paymentMethodId, @Nullable final UUID paymentId, @Nullable final UUID transactionId, @Nullable final BigDecimal amount, @Nullable final Currency currency, @Nullable final String paymentExternalKey, @Nullable final String paymentTransactionExternalKey) {
+    protected void logAPICall(final String transactionType,
+                              final Account account,
+                              final UUID paymentMethodId,
+                              @Nullable final UUID paymentId,
+                              @Nullable final UUID transactionId,
+                              @Nullable final BigDecimal amount,
+                              @Nullable final Currency currency,
+                              @Nullable final String paymentExternalKey,
+                              @Nullable final String paymentTransactionExternalKey,
+                              @Nullable final TransactionStatus transactionStatus,
+                              @Nullable final List<String> paymentControlPluginNames) {
         if (log.isInfoEnabled()) {
             final StringBuilder logLine = new StringBuilder();
-            logLine.append("PaymentApi : ")
+            logLine.append("PaymentApi: transactionType='")
                    .append(transactionType)
-                   .append(", account = ")
-                   .append(account.getId());
+                   .append("', accountId='")
+                   .append(account.getId())
+                   .append("'");
             if (paymentMethodId != null) {
-                logLine.append(", paymentMethodId = ")
-                       .append(paymentMethodId);
+                logLine.append(", paymentMethodId='")
+                       .append(paymentMethodId)
+                       .append("'");
             }
             if (paymentExternalKey != null) {
-                logLine.append(", paymentExternalKey = ")
-                       .append(paymentExternalKey);
+                logLine.append(", paymentExternalKey='")
+                       .append(paymentExternalKey)
+                       .append("'");
             }
             if (paymentTransactionExternalKey != null) {
-                logLine.append(", paymentTransactionExternalKey = ")
-                       .append(paymentTransactionExternalKey);
+                logLine.append(", paymentTransactionExternalKey='")
+                       .append(paymentTransactionExternalKey)
+                       .append("'");
             }
             if (paymentId != null) {
-                logLine.append(", paymentId = ")
-                       .append(paymentId);
+                logLine.append(", paymentId='")
+                       .append(paymentId)
+                       .append("'");
             }
             if (transactionId != null) {
-                logLine.append(", transactionId = ")
-                       .append(transactionId);
+                logLine.append(", transactionId='")
+                       .append(transactionId)
+                       .append("'");
             }
             if (amount != null) {
-                logLine.append(", amount = ")
-                       .append(amount);
+                logLine.append(", amount='")
+                       .append(amount)
+                       .append("'");
             }
             if (currency != null) {
-                logLine.append(", currency = ")
-                       .append(currency);
+                logLine.append(", currency='")
+                       .append(currency)
+                       .append("'");
+            }
+            if (transactionStatus != null) {
+                logLine.append(", transactionStatus='")
+                       .append(transactionStatus)
+                       .append("'");
+            }
+            if (paymentControlPluginNames != null) {
+                logLine.append(", paymentControlPluginNames='")
+                       .append(JOINER.join(paymentControlPluginNames))
+                       .append("'");
             }
             log.info(logLine.toString());
         }
@@ -106,11 +136,4 @@ public class DefaultApiBase {
             throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, parameterName, "should not be null");
         }
     }
-
-    protected void checkPositiveAmount(final BigDecimal amount) throws PaymentApiException {
-        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
-            throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, "amount", "should be greater than 0");
-        }
-    }
-
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
index ccb946e..2ce8fe5 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
@@ -33,7 +33,6 @@ import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.payment.core.PaymentMethodProcessor;
 import org.killbill.billing.payment.core.PaymentProcessor;
 import org.killbill.billing.payment.core.PluginControlPaymentProcessor;
-import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
 import org.killbill.billing.util.UUIDs;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
@@ -43,7 +42,8 @@ import org.killbill.billing.util.entity.Pagination;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.collect.ImmutableList;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
 
 public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
 
@@ -74,16 +74,31 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         checkNotNullParameter(paymentMethodId, "paymentMethodId");
         if (paymentId == null) {
             checkNotNullParameter(amount, "amount");
-            checkPositiveAmount(amount);
             checkNotNullParameter(currency, "currency");
         }
         checkNotNullParameter(properties, "plugin properties");
 
-        logAPICall(TransactionType.AUTHORIZE.name(), account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey);
+        final String transactionType = TransactionType.AUTHORIZE.name();
+        logAPICall(transactionType, account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey, null, null);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return paymentProcessor.createAuthorization(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
-                                                    SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
+        final Payment payment = paymentProcessor.createAuthorization(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
+                                                                     SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   null);
+
+        return payment;
     }
 
     @Override
@@ -99,16 +114,31 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         checkNotNullParameter(paymentMethodId, "paymentMethodId");
         if (paymentId == null) {
             checkNotNullParameter(amount, "amount");
-            checkPositiveAmount(amount);
             checkNotNullParameter(currency, "currency");
         }
         checkNotNullParameter(properties, "plugin properties");
 
-        logAPICall(TransactionType.AUTHORIZE.name(), account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey);
+        final String transactionType = TransactionType.AUTHORIZE.name();
+        logAPICall(transactionType, account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey, null, paymentControlPluginNames);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return pluginControlPaymentProcessor.createAuthorization(IS_API_PAYMENT, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
-                                                                 properties, paymentControlPluginNames, callContext, internalCallContext);
+        final Payment payment = pluginControlPaymentProcessor.createAuthorization(IS_API_PAYMENT, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
+                                                                                  properties, paymentControlPluginNames, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   paymentControlPluginNames);
+
+        return payment;
     }
 
     @Override
@@ -117,15 +147,31 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
 
         checkNotNullParameter(account, "account");
         checkNotNullParameter(paymentId, "paymentId");
+        checkNotNullParameter(amount, "amount");
         checkNotNullParameter(currency, "currency");
         checkNotNullParameter(properties, "plugin properties");
-        checkPositiveAmount(amount);
 
-        logAPICall(TransactionType.CAPTURE.name(), account, null, paymentId, null, amount, currency, null, paymentTransactionExternalKey);
+        final String transactionType = TransactionType.CAPTURE.name();
+        logAPICall(transactionType, account, null, paymentId, null, amount, currency, null, paymentTransactionExternalKey, null, null);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return paymentProcessor.createCapture(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, amount, currency, paymentTransactionExternalKey,
-                                              SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
+        final Payment payment = paymentProcessor.createCapture(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, amount, currency, paymentTransactionExternalKey,
+                                                               SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   null);
+
+        return payment;
     }
 
     @Override
@@ -138,13 +184,31 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
 
         checkNotNullParameter(account, "account");
         checkNotNullParameter(paymentId, "paymentId");
+        checkNotNullParameter(amount, "amount");
         checkNotNullParameter(currency, "currency");
         checkNotNullParameter(properties, "plugin properties");
-        checkPositiveAmount(amount);
+
+        final String transactionType = TransactionType.CAPTURE.name();
+        logAPICall(transactionType, account, null, paymentId, null, amount, currency, null, paymentTransactionExternalKey, null, paymentControlPluginNames);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return pluginControlPaymentProcessor.createCapture(IS_API_PAYMENT, account, paymentId, amount, currency, paymentTransactionExternalKey,
-                                                           properties, paymentControlPluginNames, callContext, internalCallContext);
+        final Payment payment = pluginControlPaymentProcessor.createCapture(IS_API_PAYMENT, account, paymentId, amount, currency, paymentTransactionExternalKey,
+                                                                            properties, paymentControlPluginNames, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   paymentControlPluginNames);
+
+        return payment;
     }
 
     @Override
@@ -154,20 +218,35 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         checkNotNullParameter(paymentMethodId, "paymentMethodId");
         if (paymentId == null) {
             checkNotNullParameter(amount, "amount");
-            checkPositiveAmount(amount);
             checkNotNullParameter(currency, "currency");
         }
         checkNotNullParameter(properties, "plugin properties");
 
-        logAPICall(TransactionType.PURCHASE.name(), account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey);
+        final String transactionType = TransactionType.PURCHASE.name();
+        logAPICall(transactionType, account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey, null, null);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return paymentProcessor.createPurchase(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
-                                               SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
+        final Payment payment = paymentProcessor.createPurchase(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
+                                                                SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   null);
+
+        return payment;
     }
 
     @Override
-    public Payment createPurchaseWithPaymentControl(final Account account, @Nullable final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency, @Nullable  final String paymentExternalKey, final String paymentTransactionExternalKey,
+    public Payment createPurchaseWithPaymentControl(final Account account, @Nullable final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
                                                     final Iterable<PluginProperty> properties, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
         final List<String> paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions);
         if (paymentControlPluginNames.isEmpty()) {
@@ -177,26 +256,39 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         checkNotNullParameter(account, "account");
         if (paymentId == null) {
             checkNotNullParameter(amount, "amount");
-            checkPositiveAmount(amount);
             checkNotNullParameter(currency, "currency");
         }
         checkNotNullParameter(paymentTransactionExternalKey, "paymentTransactionExternalKey");
         checkNotNullParameter(properties, "plugin properties");
 
-        logAPICall(TransactionType.PURCHASE.name(), account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey);
-
         if (paymentMethodId == null && !paymentOptions.isExternalPayment()) {
             throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, "paymentMethodId", "should not be null");
         }
-
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-
         final UUID nonNulPaymentMethodId = (paymentMethodId != null) ?
                                            paymentMethodId :
                                            paymentMethodProcessor.createOrGetExternalPaymentMethod(UUIDs.randomUUID().toString(), account, properties, callContext, internalCallContext);
-        return pluginControlPaymentProcessor.createPurchase(IS_API_PAYMENT, account, nonNulPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
-                                                            properties, paymentControlPluginNames, callContext, internalCallContext);
 
+        final String transactionType = TransactionType.PURCHASE.name();
+        logAPICall(transactionType, account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey, null, paymentControlPluginNames);
+
+        final Payment payment = pluginControlPaymentProcessor.createPurchase(IS_API_PAYMENT, account, nonNulPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
+                                                                             properties, paymentControlPluginNames, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   paymentControlPluginNames);
+
+        return payment;
     }
 
     @Override
@@ -207,11 +299,27 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         checkNotNullParameter(paymentId, "paymentId");
         checkNotNullParameter(properties, "plugin properties");
 
-        logAPICall(TransactionType.VOID.name(), account, null, paymentId, null, null, null, null, paymentTransactionExternalKey);
+        final String transactionType = TransactionType.VOID.name();
+        logAPICall(transactionType, account, null, paymentId, null, null, null, null, paymentTransactionExternalKey, null, null);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return paymentProcessor.createVoid(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, paymentTransactionExternalKey,
-                                           SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
+        final Payment payment = paymentProcessor.createVoid(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, paymentTransactionExternalKey,
+                                                            SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   null);
+
+        return payment;
 
     }
 
@@ -226,11 +334,27 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         checkNotNullParameter(paymentId, "paymentId");
         checkNotNullParameter(properties, "plugin properties");
 
-        logAPICall(TransactionType.VOID.name(), account, null, paymentId, null, null, null, null, paymentTransactionExternalKey);
+        final String transactionType = TransactionType.VOID.name();
+        logAPICall(transactionType, account, null, paymentId, null, null, null, null, paymentTransactionExternalKey, null, paymentControlPluginNames);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return pluginControlPaymentProcessor.createVoid(IS_API_PAYMENT, account, paymentId, paymentTransactionExternalKey,
-                                                        properties, paymentControlPluginNames, callContext, internalCallContext);
+        final Payment payment = pluginControlPaymentProcessor.createVoid(IS_API_PAYMENT, account, paymentId, paymentTransactionExternalKey,
+                                                                         properties, paymentControlPluginNames, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   paymentControlPluginNames);
+
+        return payment;
     }
 
     @Override
@@ -242,15 +366,28 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         }
         checkNotNullParameter(paymentId, "paymentId");
         checkNotNullParameter(properties, "plugin properties");
-        if (amount != null) {
-            checkPositiveAmount(amount);
-        }
 
-        logAPICall(TransactionType.REFUND.name(), account, null, paymentId, null, amount, currency, null, paymentTransactionExternalKey);
+        final String transactionType = TransactionType.REFUND.name();
+        logAPICall(transactionType, account, null, paymentId, null, amount, currency, null, paymentTransactionExternalKey, null, null);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return paymentProcessor.createRefund(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, amount, currency, paymentTransactionExternalKey,
-                                             SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
+        final Payment payment = paymentProcessor.createRefund(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, amount, currency, paymentTransactionExternalKey,
+                                                              SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   null);
+
+        return payment;
     }
 
     @Override
@@ -268,16 +405,28 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         checkNotNullParameter(paymentId, "paymentId");
         checkNotNullParameter(paymentTransactionExternalKey, "paymentTransactionExternalKey");
         checkNotNullParameter(properties, "plugin properties");
-        if (amount != null) {
-            checkPositiveAmount(amount);
-        }
 
-        logAPICall(TransactionType.REFUND.name(), account, null, paymentId, null, amount, currency, null, paymentTransactionExternalKey);
+        final String transactionType = TransactionType.REFUND.name();
+        logAPICall(transactionType, account, null, paymentId, null, amount, currency, null, paymentTransactionExternalKey, null, paymentControlPluginNames);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return pluginControlPaymentProcessor.createRefund(IS_API_PAYMENT, account, paymentId, amount, currency, paymentTransactionExternalKey,
-                                                          properties, paymentControlPluginNames, callContext, internalCallContext);
+        final Payment payment = pluginControlPaymentProcessor.createRefund(IS_API_PAYMENT, account, paymentId, amount, currency, paymentTransactionExternalKey,
+                                                                           properties, paymentControlPluginNames, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   paymentControlPluginNames);
 
+        return payment;
     }
 
     @Override
@@ -288,17 +437,31 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         checkNotNullParameter(paymentMethodId, "paymentMethodId");
         if (paymentId == null) {
             checkNotNullParameter(amount, "amount");
-            checkPositiveAmount(amount);
             checkNotNullParameter(currency, "currency");
         }
         checkNotNullParameter(properties, "plugin properties");
 
-        logAPICall(TransactionType.CREDIT.name(), account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey);
+        final String transactionType = TransactionType.CREDIT.name();
+        logAPICall(transactionType, account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey, null, null);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return paymentProcessor.createCredit(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
-                                             SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
+        final Payment payment = paymentProcessor.createCredit(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
+                                                              SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   null);
 
+        return payment;
     }
 
     @Override
@@ -314,16 +477,31 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         checkNotNullParameter(paymentMethodId, "paymentMethodId");
         if (paymentId == null) {
             checkNotNullParameter(amount, "amount");
-            checkPositiveAmount(amount);
             checkNotNullParameter(currency, "currency");
         }
         checkNotNullParameter(properties, "plugin properties");
 
-        logAPICall(TransactionType.CREDIT.name(), account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey);
+        final String transactionType = TransactionType.CREDIT.name();
+        logAPICall(transactionType, account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey, null, paymentControlPluginNames);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return pluginControlPaymentProcessor.createCredit(IS_API_PAYMENT, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
-                                                          properties, paymentControlPluginNames, callContext, internalCallContext);
+        final Payment payment = pluginControlPaymentProcessor.createCredit(IS_API_PAYMENT, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
+                                                                           properties, paymentControlPluginNames, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   paymentControlPluginNames);
+
+        return payment;
     }
 
     @Override
@@ -331,10 +509,32 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         checkNotNullParameter(account, "account");
         checkNotNullParameter(paymentTransactionId, "paymentTransactionId");
 
-        logAPICall("NOTIFY_STATE_CHANGE", account, null, null, paymentTransactionId, null, null, null, null);
+        final String transactionType = "NOTIFY_STATE_CHANGE";
+        logAPICall(transactionType, account, null, null, paymentTransactionId, null, null, null, null, null, null);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return paymentProcessor.notifyPendingPaymentOfStateChanged(account, paymentTransactionId, isSuccess, callContext, internalCallContext);
+        final Payment payment = paymentProcessor.notifyPendingPaymentOfStateChanged(account, paymentTransactionId, isSuccess, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = Iterables.<PaymentTransaction>tryFind(payment.getTransactions(),
+                                                                                            new Predicate<PaymentTransaction>() {
+                                                                                                @Override
+                                                                                                public boolean apply(final PaymentTransaction transaction) {
+                                                                                                    return transaction.getId().equals(paymentTransactionId);
+                                                                                                }
+                                                                                            }).orNull();
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction == null ? null : paymentTransaction.getId(),
+                   paymentTransaction == null ? null : paymentTransaction.getProcessedAmount(),
+                   paymentTransaction == null ? null : paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction == null ? null : paymentTransaction.getExternalKey(),
+                   paymentTransaction == null ? null : paymentTransaction.getTransactionStatus(),
+                   null);
+
+        return payment;
     }
 
     @Override
@@ -348,14 +548,28 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         checkNotNullParameter(amount, "amount");
         checkNotNullParameter(currency, "currency");
         checkNotNullParameter(paymentId, "paymentId");
-        checkPositiveAmount(amount);
 
-        logAPICall(TransactionType.CHARGEBACK.name(), account, null, paymentId, null, amount, currency, null, paymentTransactionExternalKey);
+        final String transactionType = TransactionType.CHARGEBACK.name();
+        logAPICall(transactionType, account, null, paymentId, null, amount, currency, null, paymentTransactionExternalKey, null, null);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return paymentProcessor.createChargeback(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, paymentTransactionExternalKey, amount, currency, true,
-                                                 callContext, internalCallContext);
+        final Payment payment = paymentProcessor.createChargeback(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, paymentTransactionExternalKey, amount, currency, true,
+                                                                  callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   null);
 
+        return payment;
     }
 
     @Override
@@ -369,11 +583,28 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
         checkNotNullParameter(amount, "amount");
         checkNotNullParameter(currency, "currency");
         checkNotNullParameter(paymentId, "paymentId");
-        checkPositiveAmount(amount);
+
+        final String transactionType = TransactionType.CHARGEBACK.name();
+        logAPICall(transactionType, account, null, paymentId, null, amount, currency, null, paymentTransactionExternalKey, null, paymentControlPluginNames);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        return pluginControlPaymentProcessor.createChargeback(IS_API_PAYMENT, account, paymentId, paymentTransactionExternalKey, amount, currency,
-                                                              paymentControlPluginNames, callContext, internalCallContext);
+        final Payment payment = pluginControlPaymentProcessor.createChargeback(IS_API_PAYMENT, account, paymentId, paymentTransactionExternalKey, amount, currency,
+                                                                               paymentControlPluginNames, callContext, internalCallContext);
+
+        final PaymentTransaction paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+        logAPICall(transactionType,
+                   account,
+                   payment.getPaymentMethodId(),
+                   payment.getId(),
+                   paymentTransaction.getId(),
+                   paymentTransaction.getProcessedAmount(),
+                   paymentTransaction.getProcessedCurrency(),
+                   payment.getExternalKey(),
+                   paymentTransaction.getExternalKey(),
+                   paymentTransaction.getTransactionStatus(),
+                   paymentControlPluginNames);
+
+        return payment;
     }
 
     @Override
@@ -432,7 +663,7 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
     @Override
     public List<PaymentMethod> getAccountPaymentMethods(final UUID accountId, final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext context)
             throws PaymentApiException {
-        return paymentMethodProcessor.getPaymentMethods(accountId, withPluginInfo, properties, context, internalCallContextFactory.createInternalTenantContext(accountId, context));
+        return paymentMethodProcessor.getPaymentMethods(withPluginInfo, properties, context, internalCallContextFactory.createInternalTenantContext(accountId, context));
     }
 
     @Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/bus/PaymentBusEventHandler.java b/payment/src/main/java/org/killbill/billing/payment/bus/PaymentBusEventHandler.java
index c7705ec..45f8ba0 100644
--- a/payment/src/main/java/org/killbill/billing/payment/bus/PaymentBusEventHandler.java
+++ b/payment/src/main/java/org/killbill/billing/payment/bus/PaymentBusEventHandler.java
@@ -80,8 +80,7 @@ public class PaymentBusEventHandler {
     @AllowConcurrentEvents
     @Subscribe
     public void processInvoiceEvent(final InvoiceCreationInternalEvent event) {
-        log.info("Received invoice creation notification for account {} and invoice {}",
-                 event.getAccountId(), event.getInvoiceId());
+        log.info("Received invoice creation notification for accountId='{}', invoiceId='{}'", event.getAccountId(), event.getInvoiceId());
 
         final Account account;
         try {
@@ -100,7 +99,7 @@ public class PaymentBusEventHandler {
             pluginControlPaymentProcessor.createPurchase(false, account, account.getPaymentMethodId(), null, amountToBePaid, account.getCurrency(), UUIDs.randomUUID().toString(), UUIDs.randomUUID().toString(),
                                                          properties, paymentControlPluginNames, callContext, internalContext);
         } catch (final AccountApiException e) {
-            log.error("Failed to process invoice payment", e);
+            log.warn("Failed to process invoice payment", e);
         } catch (final PaymentApiException e) {
             // Log as error unless:
             if (e.getCode() != ErrorCode.PAYMENT_NULL_INVOICE.getCode() /* Nothing left to be paid */ &&
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java
index 4803871..0444d7f 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -48,7 +48,7 @@ import org.slf4j.LoggerFactory;
 
 abstract class CompletionTaskBase<T> implements Runnable {
 
-    protected Logger log = LoggerFactory.getLogger(CompletionTaskBase.class);
+    private static final Logger log = LoggerFactory.getLogger(CompletionTaskBase.class);
 
     protected final PaymentConfig paymentConfig;
     protected final Clock clock;
@@ -125,9 +125,9 @@ abstract class CompletionTaskBase<T> implements Runnable {
             lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getExternalKey(), paymentConfig.getMaxGlobalLockRetries());
             return callback.doIteration();
         } catch (AccountApiException e) {
-            log.warn(String.format("Janitor failed to retrieve account with recordId %s", internalTenantContext.getAccountRecordId()), e);
+            log.warn("Error retrieving accountRecordId='{}'", internalTenantContext.getAccountRecordId(), e);
         } catch (LockFailedException e) {
-            log.warn(String.format("Janitor failed to lock account with recordId %s", internalTenantContext.getAccountRecordId()), e);
+            log.warn("Error locking accountRecordId='{}'", internalTenantContext.getAccountRecordId(), e);
         } finally {
             if (lock != null) {
                 lock.release();
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java
index 9851afb..0e96443 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -48,6 +48,8 @@ import org.killbill.billing.util.entity.Pagination;
 import org.killbill.clock.Clock;
 import org.killbill.commons.locker.GlobalLocker;
 import org.killbill.notificationq.api.NotificationQueue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
@@ -61,6 +63,8 @@ import com.google.common.collect.Iterables;
  */
 public class IncompletePaymentAttemptTask extends CompletionTaskBase<PaymentAttemptModelDao> {
 
+    private static final Logger log = LoggerFactory.getLogger(IncompletePaymentAttemptTask.class);
+
     //
     // Each paymentAttempt *should* transition to a new state, so fetching a limited size will still allow us to progress (as opposed to fetching the same entries over and over)
     // We also don't expect to see too many entries in the INIT state.
@@ -124,7 +128,7 @@ public class IncompletePaymentAttemptTask extends CompletionTaskBase<PaymentAtte
         if (transaction == null ||
             transaction.getTransactionStatus() == TransactionStatus.PLUGIN_FAILURE ||
             transaction.getTransactionStatus() == TransactionStatus.PAYMENT_FAILURE) {
-            log.info("Janitor AttemptCompletionTask moving attempt " + attempt.getId() + " -> ABORTED");
+            log.info("Moving attemptId='{}' to ABORTED", attempt.getId());
             paymentDao.updatePaymentAttempt(attempt.getId(), attempt.getTransactionId(), "ABORTED", internalCallContext);
             return;
         }
@@ -140,7 +144,7 @@ public class IncompletePaymentAttemptTask extends CompletionTaskBase<PaymentAtte
             transaction.getTransactionStatus() == TransactionStatus.PENDING) {
 
             try {
-                log.info("Janitor AttemptCompletionTask completing attempt " + attempt.getId() + " -> SUCCESS");
+                log.info("Moving attemptId='{}' to SUCCESS", attempt.getId());
 
                 final Account account = accountInternalApi.getAccountById(attempt.getAccountId(), tenantContext);
                 final boolean isApiPayment = true; // unclear
@@ -166,11 +170,11 @@ public class IncompletePaymentAttemptTask extends CompletionTaskBase<PaymentAtte
                 //
                 pluginControlledPaymentAutomatonRunner.completeRun(paymentStateContext);
             } catch (final AccountApiException e) {
-                log.warn("Janitor AttemptCompletionTask failed to complete payment attempt " + attempt.getId(), e);
+                log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
             } catch (final PluginPropertySerializerException e) {
-                log.warn("Janitor AttemptCompletionTask failed to complete payment attempt " + attempt.getId(), e);
+                log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
             } catch (final PaymentApiException e) {
-                log.warn("Janitor AttemptCompletionTask failed to complete payment attempt " + attempt.getId(), e);
+                log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
             }
         }
     }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
index c71407d..29a722e 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -54,6 +54,8 @@ import org.killbill.commons.locker.GlobalLocker;
 import org.killbill.notificationq.api.NotificationEvent;
 import org.killbill.notificationq.api.NotificationQueue;
 import org.skife.config.TimeSpan;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
@@ -64,6 +66,8 @@ import com.google.common.collect.Iterables;
 
 public class IncompletePaymentTransactionTask extends CompletionTaskBase<PaymentTransactionModelDao> {
 
+    private static final Logger log = LoggerFactory.getLogger(IncompletePaymentTransactionTask.class);
+
     private static final ImmutableList<TransactionStatus> TRANSACTION_STATUSES_TO_CONSIDER = ImmutableList.<TransactionStatus>builder()
                                                                                                           .add(TransactionStatus.PENDING)
                                                                                                           .add(TransactionStatus.UNKNOWN)
@@ -192,7 +196,7 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
             case UNKNOWN:
             default:
                 if (transactionStatus != paymentTransaction.getTransactionStatus()) {
-                    log.info("Janitor IncompletePaymentTransactionTask unable to repair payment {}, transaction {}: {} -> {}",
+                    log.info("Unable to repair paymentId='{}', paymentTransactionId='{}', currentTransactionStatus='{}', newTransactionStatus='{}'",
                              payment.getId(), paymentTransaction.getId(), paymentTransaction.getTransactionStatus(), transactionStatus);
                 }
                 // We can't get anything interesting from the plugin...
@@ -233,8 +237,7 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
         final String gatewayErrorCode = paymentTransactionInfoPlugin != null ? paymentTransactionInfoPlugin.getGatewayErrorCode() : paymentTransaction.getGatewayErrorCode();
         final String gatewayError = paymentTransactionInfoPlugin != null ? paymentTransactionInfoPlugin.getGatewayError() : paymentTransaction.getGatewayErrorMsg();
 
-
-        log.info("Janitor IncompletePaymentTransactionTask repairing payment {}, transaction {}, transitioning transactionStatus from {} -> {}",
+        log.info("Repairing paymentId='{}', paymentTransactionId='{}', currentTransactionStatus='{}', newTransactionStatus='{}'",
                  payment.getId(), paymentTransaction.getId(), paymentTransaction.getTransactionStatus(), transactionStatus);
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(payment.getAccountId(), callContext);
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java
index fbac8e0..c17f17c 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java
@@ -135,7 +135,7 @@ public class Janitor {
                                                                             @Override
                                                                             public void handleReadyNotification(final NotificationEvent notificationKey, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
                                                                                 if (!(notificationKey instanceof JanitorNotificationKey)) {
-                                                                                    log.error("Janitor service received an unexpected event type {}" + notificationKey.getClass().getName());
+                                                                                    log.error("Janitor service received an unexpected event className='{}", notificationKey.getClass());
                                                                                     return;
 
                                                                                 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentExecutors.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentExecutors.java
index 18fcb1e..d58c304 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentExecutors.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentExecutors.java
@@ -21,6 +21,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
 import javax.inject.Inject;
@@ -43,7 +44,7 @@ public class PaymentExecutors {
 
     private final PaymentConfig paymentConfig;
 
-    private volatile ExecutorService pluginExecutorService;
+    private volatile ThreadPoolExecutor pluginExecutorService;
     private volatile ScheduledExecutorService janitorExecutorService;
 
     @Inject
@@ -54,6 +55,7 @@ public class PaymentExecutors {
 
     public void initialize() {
         this.pluginExecutorService = createPluginExecutorService();
+        this.pluginExecutorService.prestartAllCoreThreads();
         this.janitorExecutorService = createJanitorExecutorService();
     }
 
@@ -77,7 +79,7 @@ public class PaymentExecutors {
         return janitorExecutorService;
     }
 
-    private ExecutorService createPluginExecutorService() {
+    private ThreadPoolExecutor createPluginExecutorService() {
         final int minThreadNb = DEFAULT_MIN_PLUGIN_THREADS < paymentConfig.getPaymentPluginThreadNb() ? DEFAULT_MIN_PLUGIN_THREADS : paymentConfig.getPaymentPluginThreadNb();
         return new WithProfilingThreadPoolExecutor(minThreadNb,
                                                    paymentConfig.getPaymentPluginThreadNb(),
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
index 0429c46..e8324fb 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
@@ -139,7 +139,6 @@ public class PaymentMethodProcessor extends ProcessorBase {
                                                                                                                     accountInternalApi.updatePaymentMethod(account.getId(), pm.getId(), context);
                                                                                                                 }
                                                                                                             } catch (final PaymentPluginApiException e) {
-                                                                                                                log.warn("Error adding payment method " + pm.getId() + " for plugin " + paymentPluginServiceName, e);
                                                                                                                 throw new PaymentApiException(ErrorCode.PAYMENT_ADD_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
                                                                                                             } catch (final AccountApiException e) {
                                                                                                                 throw new PaymentApiException(e);
@@ -150,7 +149,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
 
                                                                                                         private void validateUniqueExternalPaymentMethod(final UUID accountId, final String pluginName) throws PaymentApiException {
                                                                                                             if (ExternalPaymentProviderPlugin.PLUGIN_NAME.equals(pluginName)) {
-                                                                                                                final List<PaymentMethodModelDao> accountPaymentMethods = paymentDao.getPaymentMethods(accountId, context);
+                                                                                                                final List<PaymentMethodModelDao> accountPaymentMethods = paymentDao.getPaymentMethods(context);
                                                                                                                 if (Iterables.any(accountPaymentMethods, new Predicate<PaymentMethodModelDao>() {
                                                                                                                     @Override
                                                                                                                     public boolean apply(final PaymentMethodModelDao input) {
@@ -176,7 +175,11 @@ public class PaymentMethodProcessor extends ProcessorBase {
         try {
             paymentMethodPlugin = pluginApi.getPaymentMethodDetail(account.getId(), pm.getId(), properties, callContext);
         } catch (final PaymentPluginApiException e) {
-            log.warn("Error retrieving payment method " + pm.getId() + " from plugin " + pm.getPluginName(), e);
+            if (e.getCause() == null) {
+                log.warn("Error retrieving paymentMethodId='{}', plugin='{}', errorMessage='{}', errorType='{}'", pm.getId(), pm.getPluginName(), e.getErrorMessage(), e.getErrorType());
+            } else {
+                log.warn("Error retrieving paymentMethodId='{}', plugin='{}', errorMessage='{}', errorType='{}'", pm.getId(), pm.getPluginName(), e.getErrorMessage(), e.getErrorType(), e);
+            }
             return null;
         }
 
@@ -189,12 +192,12 @@ public class PaymentMethodProcessor extends ProcessorBase {
         }
     }
 
-    public List<PaymentMethod> getPaymentMethods(final UUID accountId, final boolean withPluginInfo, final Iterable<PluginProperty> properties, final InternalTenantContext context) throws PaymentApiException {
-        return getPaymentMethods(accountId, withPluginInfo, properties, buildTenantContext(context), context);
+    public List<PaymentMethod> getPaymentMethods(final boolean withPluginInfo, final Iterable<PluginProperty> properties, final InternalTenantContext context) throws PaymentApiException {
+        return getPaymentMethods(withPluginInfo, properties, buildTenantContext(context), context);
     }
 
-    public List<PaymentMethod> getPaymentMethods(final UUID accountId, final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext context) throws PaymentApiException {
-        final List<PaymentMethodModelDao> paymentMethodModels = paymentDao.getPaymentMethods(accountId, context);
+    public List<PaymentMethod> getPaymentMethods(final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext context) throws PaymentApiException {
+        final List<PaymentMethodModelDao> paymentMethodModels = paymentDao.getPaymentMethods(context);
         if (paymentMethodModels.isEmpty()) {
             return Collections.emptyList();
         }
@@ -227,7 +230,6 @@ public class PaymentMethodProcessor extends ProcessorBase {
                 final PaymentPluginApi pluginApi = getPaymentPluginApi(paymentMethodModelDao.getPluginName());
                 paymentMethodPlugin = pluginApi.getPaymentMethodDetail(paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId(), properties, tenantContext);
             } catch (final PaymentPluginApiException e) {
-                log.warn("Error retrieving payment method " + paymentMethodModelDao.getId() + " from plugin " + paymentMethodModelDao.getPluginName(), e);
                 throw new PaymentApiException(ErrorCode.PAYMENT_GET_PAYMENT_METHODS, paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId());
             }
         } else {
@@ -270,7 +272,11 @@ public class PaymentMethodProcessor extends ProcessorBase {
                                                try {
                                                    paymentMethodPlugin = pluginApi.getPaymentMethodDetail(paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId(), properties, tenantContext);
                                                } catch (final PaymentPluginApiException e) {
-                                                   log.warn("Unable to find payment method id " + paymentMethodModelDao.getId() + " in plugin " + pluginName);
+                                                   if (e.getCause() == null) {
+                                                       log.warn("Error retrieving paymentMethodId='{}', plugin='{}', errorMessage='{}', errorType='{}'", paymentMethodModelDao.getId(), pluginName, e.getErrorMessage(), e.getErrorType());
+                                                   } else {
+                                                       log.warn("Error retrieving paymentMethodId='{}', plugin='{}', errorMessage='{}', errorType='{}'", paymentMethodModelDao.getId(), pluginName, e.getErrorMessage(), e.getErrorType(), e);
+                                                   }
                                                    // We still want to return a payment method object, even though the plugin details are missing
                                                }
                                            }
@@ -353,8 +359,8 @@ public class PaymentMethodProcessor extends ProcessorBase {
                                   );
     }
 
-    public PaymentMethod getExternalPaymentMethod(final UUID accountId, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext context) throws PaymentApiException {
-        final List<PaymentMethod> paymentMethods = getPaymentMethods(accountId, false, properties, tenantContext, context);
+    public PaymentMethod getExternalPaymentMethod(final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext context) throws PaymentApiException {
+        final List<PaymentMethod> paymentMethods = getPaymentMethods(false, properties, tenantContext, context);
         for (final PaymentMethod paymentMethod : paymentMethods) {
             if (ExternalPaymentProviderPlugin.PLUGIN_NAME.equals(paymentMethod.getPluginName())) {
                 return paymentMethod;
@@ -366,7 +372,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
     public UUID createOrGetExternalPaymentMethod(final String paymentMethodExternalKey, final Account account, final Iterable<PluginProperty> properties, final CallContext callContext, final InternalCallContext context) throws PaymentApiException {
         // Check if this account has already used the external payment plugin
         // If not, it's the first time - add a payment method for it
-        final PaymentMethod externalPaymentMethod = getExternalPaymentMethod(account.getId(), properties, callContext, context);
+        final PaymentMethod externalPaymentMethod = getExternalPaymentMethod(properties, callContext, context);
         if (externalPaymentMethod != null) {
             return externalPaymentMethod.getId();
         }
@@ -413,7 +419,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
                             } else {
                                 final boolean isAccountAutoPayOff = isAccountAutoPayOff(account.getId(), context);
                                 if (!isAccountAutoPayOff) {
-                                    log.info("Setting account {} to AUTO_PAY_OFF because of default payment method deletion", account.getId());
+                                    log.info("Setting AUTO_PAY_OFF on accountId='{}' because of default payment method deletion", account.getId());
                                     setAccountAutoPayOff(account.getId(), context);
                                 }
                                 accountInternalApi.removePaymentMethod(account.getId(), context);
@@ -424,7 +430,6 @@ public class PaymentMethodProcessor extends ProcessorBase {
                         paymentDao.deletedPaymentMethod(paymentMethodId, context);
                         return PluginDispatcher.createPluginDispatcherReturnType(null);
                     } catch (final PaymentPluginApiException e) {
-                        log.warn("Error deleting payment method " + paymentMethodId, e);
                         throw new PaymentApiException(ErrorCode.PAYMENT_DEL_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
                     } catch (final AccountApiException e) {
                         throw new PaymentApiException(e);
@@ -502,7 +507,6 @@ public class PaymentMethodProcessor extends ProcessorBase {
                 return ImmutableList.<PaymentMethod>of();
             }
         } catch (final PaymentPluginApiException e) {
-            log.warn("Error refreshing payment methods for account " + account.getId() + " and plugin " + pluginName, e);
             throw new PaymentApiException(ErrorCode.PAYMENT_REFRESH_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
         }
 
@@ -534,15 +538,13 @@ public class PaymentMethodProcessor extends ProcessorBase {
                         }
                     }
 
-                    final List<PaymentMethodModelDao> refreshedPaymentMethods = paymentDao.refreshPaymentMethods(account.getId(),
-                                                                                                                 pluginName,
+                    final List<PaymentMethodModelDao> refreshedPaymentMethods = paymentDao.refreshPaymentMethods(pluginName,
                                                                                                                  finalPaymentMethods,
                                                                                                                  context);
 
                     try {
                         pluginApi.resetPaymentMethods(account.getId(), pluginPmsWithId, properties, callContext);
                     } catch (final PaymentPluginApiException e) {
-                        log.warn("Error resetting payment methods for account " + account.getId() + " and plugin " + pluginName, e);
                         throw new PaymentApiException(ErrorCode.PAYMENT_REFRESH_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
                     }
                     try {
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
index 85ba858..17e87d8 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
@@ -51,12 +51,16 @@ import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.clock.Clock;
 import org.killbill.commons.locker.GlobalLocker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 
 public class PluginControlPaymentProcessor extends ProcessorBase {
 
+    private static final Logger log = LoggerFactory.getLogger(PluginControlPaymentProcessor.class);
+
     private static final Joiner JOINER = Joiner.on(", ");
 
     private final PluginControlPaymentAutomatonRunner pluginControlledPaymentAutomatonRunner;
@@ -225,20 +229,20 @@ public class PluginControlPaymentProcessor extends ProcessorBase {
                                                        internalCallContext);
 
         } catch (final AccountApiException e) {
-            log.warn("Failed to retry attempt " + attemptId + toPluginNamesOnError(" for plugins ", paymentControlPluginNames), e);
+            log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
         } catch (final PaymentApiException e) {
-            log.warn("Failed to retry attempt " + attemptId + toPluginNamesOnError(" for plugins ", paymentControlPluginNames), e);
+            log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
         } catch (final PluginPropertySerializerException e) {
-            log.warn("Failed to retry attempt " + attemptId + toPluginNamesOnError(" for plugins ", paymentControlPluginNames), e);
+            log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
         } catch (final MissingEntryException e) {
-            log.warn("Failed to retry attempt " + attemptId + toPluginNamesOnError(" for plugins ", paymentControlPluginNames), e);
+            log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
         }
     }
 
-    private String toPluginNamesOnError(final String prefixMessage, final Collection<String> paymentControlPluginNames) {
+    private String toPluginNamesOnError(final Collection<String> paymentControlPluginNames) {
         if (paymentControlPluginNames == null || paymentControlPluginNames.isEmpty()) {
             return "";
         }
-        return prefixMessage + "(" + JOINER.join(paymentControlPluginNames) + ")";
+        return JOINER.join(paymentControlPluginNames);
     }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java b/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
index dd41668..31ac463 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
@@ -59,6 +59,8 @@ import com.google.common.collect.Collections2;
 
 public abstract class ProcessorBase {
 
+    private static final Logger log = LoggerFactory.getLogger(ProcessorBase.class);
+
     protected final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry;
     protected final AccountInternalApi accountInternalApi;
     protected final GlobalLocker locker;
@@ -66,8 +68,6 @@ public abstract class ProcessorBase {
     protected final InternalCallContextFactory internalCallContextFactory;
     protected final TagInternalApi tagInternalApi;
     protected final Clock clock;
-
-    protected static final Logger log = LoggerFactory.getLogger(ProcessorBase.class);
     protected final InvoiceInternalApi invoiceApi;
 
     public ProcessorBase(final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry,
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java
index 7d4e20b..7f0ba27 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java
@@ -187,9 +187,9 @@ public class ControlPluginRunner {
                     }
                     // Exceptions from the control plugins are ignored (and logged) because the semantics on what to do are undefined.
                 } catch (final PaymentControlApiException e) {
-                    log.warn("Plugin " + pluginName + " failed to complete executePluginOnSuccessCalls call for " + inputPaymentControlContext.getPaymentExternalKey(), e);
+                    log.warn("Error during onSuccessCall for plugin='{}', paymentExternalKey='{}'", pluginName, inputPaymentControlContext.getPaymentExternalKey(), e);
                 } catch (final RuntimeException e) {
-                    log.warn("Plugin " + pluginName + " failed to complete executePluginOnSuccessCalls call for " + inputPaymentControlContext.getPaymentExternalKey(), e);
+                    log.warn("Error during onSuccessCall for plugin='{}', paymentExternalKey='{}'", pluginName, inputPaymentControlContext.getPaymentExternalKey(), e);
                 }
             }
         }
@@ -247,7 +247,7 @@ public class ControlPluginRunner {
                     }
 
                 } catch (final PaymentControlApiException e) {
-                    log.warn("Plugin " + pluginName + " failed to return next retryDate for payment " + inputPaymentControlContext.getPaymentExternalKey(), e);
+                    log.warn("Error during onFailureCall for plugin='{}', paymentExternalKey='{}'", pluginName, inputPaymentControlContext.getPaymentExternalKey(), e);
                     return new DefaultFailureCallResult(candidate, inputPluginProperties);
                 }
             }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
index a463954..22b0d54 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
@@ -150,31 +150,32 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
                 } catch (final RuntimeException e) {
                     // Attempts to set the retry date in context if needed.
                     executePluginOnFailureCallsAndSetRetryDate(paymentControlContext);
-                    throw e;
+                    throw new OperationException(e, OperationResult.EXCEPTION);
                 }
             }
         });
     }
 
     @Override
-    protected OperationException unwrapExceptionFromDispatchedTask(final PaymentStateContext paymentStateContext, final Exception e) {
-
+    protected OperationException unwrapExceptionFromDispatchedTask(final Exception e) {
         // If this is an ExecutionException we attempt to extract the cause first
-        final Throwable originalExceptionOrCause = e instanceof ExecutionException ? MoreObjects.firstNonNull(e.getCause(), e) : e;
+        final Throwable originalExceptionOrCausePossiblyOperationException = e instanceof ExecutionException ? MoreObjects.firstNonNull(e.getCause(), e) : e;
+
+        // Unwrap OperationException too (doOperationCallback wraps exceptions in OperationException)
+        final Throwable originalExceptionOrCause = originalExceptionOrCausePossiblyOperationException instanceof OperationException ? MoreObjects.firstNonNull(originalExceptionOrCausePossiblyOperationException.getCause(), originalExceptionOrCausePossiblyOperationException) : originalExceptionOrCausePossiblyOperationException;
 
         if (originalExceptionOrCause instanceof OperationException) {
             return (OperationException) originalExceptionOrCause;
         } else if (originalExceptionOrCause instanceof LockFailedException) {
-            final String format = String.format("Failed to lock account %s", paymentStateContext.getAccount().getExternalKey());
-            logger.error(String.format(format));
+            logger.warn("Failed to lock accountId='{}'", paymentStateContext.getAccount().getId());
         } else if (originalExceptionOrCause instanceof TimeoutException) {
-            logger.warn("RetryOperationCallback call TIMEOUT for account {}", paymentStateContext.getAccount().getExternalKey());
+            logger.warn("Call TIMEOUT for accountId='{}'", paymentStateContext.getAccount().getId());
         } else if (originalExceptionOrCause instanceof InterruptedException) {
-            logger.error("RetryOperationCallback call was interrupted for account {}", paymentStateContext.getAccount().getExternalKey());
-        } else /* most probably RuntimeException */ {
-            logger.warn("RetryOperationCallback failed for account {}", paymentStateContext.getAccount().getExternalKey(), e);
+            logger.warn("Call was interrupted for accountId='{}'", paymentStateContext.getAccount().getId());
+        } else {
+            logger.warn("Operation failed for accountId='{}'", paymentStateContext.getAccount().getId(), e);
         }
-        return new OperationException(e, getOperationResultOnException(paymentStateContext));
+        return new OperationException(originalExceptionOrCause, getOperationResultOnException(paymentStateContext));
     }
 
     private OperationResult getOperationResultOnException(final PaymentStateContext paymentStateContext) {
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java
index f5c542d..1f52802 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java
@@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory;
 
 public abstract class OperationCallbackBase<CallbackOperationResult, CallbackOperationException extends Exception> {
 
-    protected final Logger logger = LoggerFactory.getLogger(OperationCallbackBase.class);
+    private final Logger logger = LoggerFactory.getLogger(OperationCallbackBase.class);
 
     private final GlobalLocker locker;
     private final PluginDispatcher<OperationResult> paymentPluginDispatcher;
@@ -72,14 +72,14 @@ public abstract class OperationCallbackBase<CallbackOperationResult, CallbackOpe
             logger.debug("Successful plugin(s) call of {} for account {} with result {}", pluginNames, account.getExternalKey(), operationResult);
             return operationResult;
         } catch (final ExecutionException e) {
-            throw unwrapExceptionFromDispatchedTask(paymentStateContext, e);
+            throw unwrapExceptionFromDispatchedTask(e);
         } catch (final TimeoutException e) {
             logger.warn("TimeoutException while executing the plugin(s) {}", pluginNames);
-            throw unwrapExceptionFromDispatchedTask(paymentStateContext, e);
+            throw unwrapExceptionFromDispatchedTask(e);
         } catch (final InterruptedException e) {
             Thread.currentThread().interrupt();
             logger.warn("InterruptedException while executing the following plugin(s): {}", pluginNames);
-            throw unwrapExceptionFromDispatchedTask(paymentStateContext, e);
+            throw unwrapExceptionFromDispatchedTask(e);
         }
     }
 
@@ -91,5 +91,5 @@ public abstract class OperationCallbackBase<CallbackOperationResult, CallbackOpe
     //
     protected abstract CallbackOperationResult doCallSpecificOperationCallback() throws CallbackOperationException;
 
-    protected abstract OperationException unwrapExceptionFromDispatchedTask(final PaymentStateContext paymentStateContext, final Exception e);
+    protected abstract OperationException unwrapExceptionFromDispatchedTask(final Exception e);
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentEnteringStateCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentEnteringStateCallback.java
index f5dd2f2..d336109 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentEnteringStateCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentEnteringStateCallback.java
@@ -76,7 +76,7 @@ public abstract class PaymentEnteringStateCallback implements EnteringStateCallb
             try {
                 daoHelper.getEventBus().post(event);
             } catch (EventBusException e) {
-                logger.error("Failed to post Payment event event for account {} ", paymentStateContext.getAccount().getId(), e);
+                logger.warn("Failed to post event {}", event, e);
             }
         }
     }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java
index edc6f70..831b349 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java
@@ -18,6 +18,9 @@
 package org.killbill.billing.payment.core.sm.payments;
 
 import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
 
 import org.killbill.automaton.OperationException;
 import org.killbill.automaton.State;
@@ -25,6 +28,7 @@ import org.killbill.automaton.State.LeavingStateCallback;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.api.TransactionStatus;
+import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.payment.core.sm.PaymentAutomatonDAOHelper;
 import org.killbill.billing.payment.core.sm.PaymentStateContext;
 import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
@@ -58,29 +62,38 @@ public abstract class PaymentLeavingStateCallback implements LeavingStateCallbac
                 throw new PaymentApiException(ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD, paymentStateContext.getAccount().getId());
             }
 
+            // If we were given a paymentId (or existing paymentExternalId -> effectivePaymentId) we first fetch existing transactions (required for sanity and handling PENDING transactions)
+            final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment = paymentStateContext.getPaymentId() != null ?
+                                                                                          daoHelper.getPaymentDao().getTransactionsForPayment(paymentStateContext.getPaymentId(), paymentStateContext.getInternalCallContext()) :
+                                                                                          ImmutableList.<PaymentTransactionModelDao>of();
+
             //
             // Extract existing transaction matching the transactionId if specified (for e.g notifyPendingTransactionOfStateChanged), or based on transactionExternalKey
             //
-            final List<PaymentTransactionModelDao> existingPaymentTransactions;
-            if (paymentStateContext.getTransactionId() != null) {
-                final PaymentTransactionModelDao transactionModelDao = daoHelper.getPaymentDao().getPaymentTransaction(paymentStateContext.getTransactionId(), paymentStateContext.getInternalCallContext());
-                existingPaymentTransactions = ImmutableList.of(transactionModelDao);
-            } else if (paymentStateContext.getPaymentTransactionExternalKey() != null) {
-                existingPaymentTransactions = daoHelper.getPaymentDao().getPaymentTransactionsByExternalKey(paymentStateContext.getPaymentTransactionExternalKey(), paymentStateContext.getInternalCallContext());
-            } else {
-                existingPaymentTransactions = ImmutableList.of();
-            }
+            final Iterable<PaymentTransactionModelDao> existingPaymentTransactionsForTransactionIdOrKey = filterExistingPaymentTransactionsForTransactionIdOrKey(paymentTransactionsForCurrentPayment, paymentStateContext.getTransactionId(), paymentStateContext.getPaymentTransactionExternalKey());
 
             // Validate the payment transactions belong to the right payment
-            validatePaymentId(existingPaymentTransactions);
+            validatePaymentIdAndTransactionType(existingPaymentTransactionsForTransactionIdOrKey);
 
             // Validate some constraints on the unicity of that paymentTransactionExternalKey
-            validateUniqueTransactionExternalKey(existingPaymentTransactions);
+            validateUniqueTransactionExternalKey(existingPaymentTransactionsForTransactionIdOrKey);
+
+            //
+            // Handle PENDING case:
+            // a) If we have a PENDING transaction for the same (payment transaction) key, this is a completion and we want to re-use the same transaction
+            // b) If we have a PENDING transaction for a different (payment transaction) key, and for an initial request (AUTH, PURCHASE, CREDIT), we FAIL the request
+            //   (unfortunately this cannot be caught by the state machine because the transition XXX_PENDING -> _SUCCESS needs to be allowed and this is irrespective of the keys)
+            // c) If we have a PENDING transaction for a different (payment transaction) key, and for other follow-up request  (CAPTURE, REFUND, ..), we ignore it and create a new transaction
+            //
+            final Iterable<PaymentTransactionModelDao> pendingTransactionsForPaymentAndTransactionType = filterPendingTransactionsForPaymentAndTransactionType(paymentTransactionsForCurrentPayment, paymentStateContext.getTransactionType());
 
-            // Handle PENDING cases, where we want to re-use the same transaction
-            final PaymentTransactionModelDao pendingPaymentTransaction = getPendingPaymentTransaction(existingPaymentTransactions);
+            // Case b)
+            validateUniqueInitialPendingTransaction(pendingTransactionsForPaymentAndTransactionType, paymentStateContext.getTransactionType(), paymentStateContext.getPaymentTransactionExternalKey());
+
+
+            final PaymentTransactionModelDao pendingPaymentTransaction = filterPendingTransactionsForTransactionKey(pendingTransactionsForPaymentAndTransactionType, paymentStateContext.getPaymentTransactionExternalKey());
             if (pendingPaymentTransaction != null) {
-                // Set the current paymentTransaction in the context (needed for the state machine logic)
+                // Case a) Set the current paymentTransaction in the context (needed for the state machine logic)
                 paymentStateContext.setPaymentTransactionModelDao(pendingPaymentTransaction);
                 return;
             }
@@ -93,25 +106,61 @@ public abstract class PaymentLeavingStateCallback implements LeavingStateCallbac
         }
     }
 
-    protected PaymentTransactionModelDao getUnknownPaymentTransaction(final List<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
-        return Iterables.tryFind(existingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
+    private void validateUniqueInitialPendingTransaction(final Iterable<PaymentTransactionModelDao> pendingTransactionsForPaymentAndTransactionType, final TransactionType transactionType, final String paymentTransactionExternalKey) {
+        if (transactionType != TransactionType.AUTHORIZE &&
+            transactionType != TransactionType.PURCHASE &&
+            transactionType != TransactionType.CREDIT) {
+            return;
+        }
+
+        final PaymentTransactionModelDao existingPendingTransactionForDifferentKey = Iterables.tryFind(pendingTransactionsForPaymentAndTransactionType, new Predicate<PaymentTransactionModelDao>() {
             @Override
             public boolean apply(final PaymentTransactionModelDao input) {
-                return input.getTransactionStatus() == TransactionStatus.UNKNOWN;
+                return !input.getTransactionExternalKey().equals(paymentTransactionExternalKey);
             }
         }).orNull();
+        if (existingPendingTransactionForDifferentKey !=  null) {
+            // We are missing ErrorCode PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS (should be fixed in 0.17.0. See #525)
+            throw new RuntimeException(String.format("Failed to create another initial transaction for paymentId='%s' : Existing PENDING transactionId='%s'",
+                                                          existingPendingTransactionForDifferentKey.getPaymentId(), existingPendingTransactionForDifferentKey.getId()));
+        }
     }
 
-    protected PaymentTransactionModelDao getPendingPaymentTransaction(final List<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
-        return Iterables.tryFind(existingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
+    protected Iterable<PaymentTransactionModelDao> filterExistingPaymentTransactionsForTransactionIdOrKey(final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment, @Nullable final UUID paymentTransactionId, @Nullable final String paymentTransactionExternalKey) throws PaymentApiException {
+        return Iterables.filter(paymentTransactionsForCurrentPayment, new Predicate<PaymentTransactionModelDao>() {
             @Override
             public boolean apply(final PaymentTransactionModelDao input) {
-                return input.getTransactionStatus() == TransactionStatus.PENDING;
+                if (paymentTransactionId != null && input.getId().equals(paymentTransactionId)) {
+                    return true;
+                }
+                if (paymentTransactionExternalKey != null && input.getTransactionExternalKey().equals(paymentTransactionExternalKey)) {
+                    return true;
+                }
+                return false;
+            }
+        });
+    }
+
+    protected Iterable<PaymentTransactionModelDao> filterPendingTransactionsForPaymentAndTransactionType(final Iterable<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment, final TransactionType transactionType) throws PaymentApiException {
+        return Iterables.filter(paymentTransactionsForCurrentPayment, new Predicate<PaymentTransactionModelDao>() {
+            @Override
+            public boolean apply(final PaymentTransactionModelDao input) {
+                return input.getTransactionStatus() == TransactionStatus.PENDING &&
+                       input.getTransactionType() == transactionType;
+            }
+        });
+    }
+
+    protected PaymentTransactionModelDao filterPendingTransactionsForTransactionKey(final Iterable<PaymentTransactionModelDao> existingPendingPaymentTransactions, final String paymentTransactionExternalKey) throws PaymentApiException {
+        return Iterables.tryFind(existingPendingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
+            @Override
+            public boolean apply(final PaymentTransactionModelDao input) {
+                return input.getTransactionExternalKey().equals(paymentTransactionExternalKey);
             }
         }).orNull();
     }
 
-    protected void validateUniqueTransactionExternalKey(final List<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
+    protected void validateUniqueTransactionExternalKey(final Iterable<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
         // If no key specified, system will allocate a unique one later, there is nothing to check
         if (paymentStateContext.getPaymentTransactionExternalKey() == null) {
             return;
@@ -134,11 +183,15 @@ public abstract class PaymentLeavingStateCallback implements LeavingStateCallbac
     }
 
     // At this point, the payment id should have been populated for follow-up transactions (see PaymentAutomationRunner#run)
-    protected void validatePaymentId(final List<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
+    protected void validatePaymentIdAndTransactionType(final Iterable<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
         for (final PaymentTransactionModelDao paymentTransactionModelDao : existingPaymentTransactions) {
             if (!paymentTransactionModelDao.getPaymentId().equals(paymentStateContext.getPaymentId())) {
                 throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, paymentTransactionModelDao.getId(), "does not belong to payment " + paymentStateContext.getPaymentId());
             }
+            if (paymentStateContext.getTransactionType() != null && paymentTransactionModelDao.getTransactionType() != paymentStateContext.getTransactionType()) {
+                throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, paymentTransactionModelDao.getId(), "has a transaction type of " + paymentTransactionModelDao.getTransactionType() +
+                                                                                                                       " instead of requested " + paymentStateContext.getTransactionType());
+            }
         }
     }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java
index 4b11c6b..e269932 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java
@@ -45,6 +45,8 @@ import org.killbill.billing.payment.provider.DefaultNoOpPaymentInfoPlugin;
 import org.killbill.billing.util.config.PaymentConfig;
 import org.killbill.commons.locker.GlobalLocker;
 import org.killbill.commons.locker.LockFailedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicate;
@@ -54,6 +56,8 @@ import com.google.common.collect.Iterables;
 // Encapsulates the payment specific logic
 public abstract class PaymentOperation extends OperationCallbackBase<PaymentTransactionInfoPlugin, PaymentPluginApiException> implements OperationCallback {
 
+    private final Logger logger = LoggerFactory.getLogger(PaymentOperation.class);
+
     protected final PaymentAutomatonDAOHelper daoHelper;
     protected PaymentPluginApi plugin;
 
@@ -68,41 +72,49 @@ public abstract class PaymentOperation extends OperationCallbackBase<PaymentTran
 
     @Override
     public OperationResult doOperationCallback() throws OperationException {
+        final String pluginName;
         try {
-            final String pluginName = daoHelper.getPaymentProviderPluginName();
+            pluginName = daoHelper.getPaymentProviderPluginName();
             this.plugin = daoHelper.getPaymentPluginApi(pluginName);
+        } catch (final PaymentApiException e) {
+            throw convertToUnknownTransactionStatusAndErroredPaymentState(e);
+        }
 
-            if (paymentStateContext.shouldLockAccountAndDispatch()) {
-                return doOperationCallbackWithDispatchAndAccountLock(pluginName);
-            } else {
+        if (paymentStateContext.shouldLockAccountAndDispatch()) {
+            // This will already call unwrapExceptionFromDispatchedTask
+            return doOperationCallbackWithDispatchAndAccountLock(pluginName);
+        } else {
+            try {
                 return doSimpleOperationCallback();
+            } catch (final Exception e) {
+                // We need to unwrap OperationException (see doSimpleOperationCallback below)
+                throw unwrapExceptionFromDispatchedTask(e);
             }
-        } catch (final Exception e) {
-            throw convertToUnknownTransactionStatusAndErroredPaymentState(e);
         }
     }
 
     @Override
-    protected OperationException unwrapExceptionFromDispatchedTask(final PaymentStateContext paymentStateContext, final Exception e) {
-
+    protected OperationException unwrapExceptionFromDispatchedTask(final Exception e) {
         // If this is an ExecutionException we attempt to extract the cause first
-        final Throwable originalExceptionOrCause = e instanceof ExecutionException ? MoreObjects.firstNonNull(e.getCause(), e) : e;
+        final Throwable originalExceptionOrCausePossiblyOperationException = e instanceof ExecutionException ? MoreObjects.firstNonNull(e.getCause(), e) : e;
+
+        // Unwrap OperationException too (doOperationCallback wraps exceptions in OperationException)
+        final Throwable originalExceptionOrCause = originalExceptionOrCausePossiblyOperationException instanceof OperationException ? MoreObjects.firstNonNull(originalExceptionOrCausePossiblyOperationException.getCause(), originalExceptionOrCausePossiblyOperationException) : originalExceptionOrCausePossiblyOperationException;
 
         //
         // Any case of exception (checked or runtime) should lead to a TransactionStatus.UNKNOWN (and a XXX_ERRORED payment state).
         // In order to reach that state we create PaymentTransactionInfoPlugin with an PaymentPluginStatus.UNDEFINED status (and an OperationResult.EXCEPTION).
         //
         if (originalExceptionOrCause instanceof LockFailedException) {
-            logger.warn("Failed to lock account {}", paymentStateContext.getAccount().getExternalKey());
+            logger.warn("Failed to lock accountExternalKey='{}'", paymentStateContext.getAccount().getExternalKey());
         } else if (originalExceptionOrCause instanceof TimeoutException) {
-            logger.error("Plugin call TIMEOUT for account {}", paymentStateContext.getAccount().getExternalKey());
+            logger.warn("Plugin call TIMEOUT for accountExternalKey='{}'", paymentStateContext.getAccount().getExternalKey());
         } else if (originalExceptionOrCause instanceof InterruptedException) {
-            logger.error("Plugin call was interrupted for account {}", paymentStateContext.getAccount().getExternalKey());
+            logger.warn("Plugin call was interrupted for accountExternalKey='{}'", paymentStateContext.getAccount().getExternalKey());
         } else {
-            logger.warn("Payment plugin call threw an exception for account {}", paymentStateContext.getAccount().getExternalKey(), originalExceptionOrCause);
+            logger.warn("Payment plugin call threw an exception for accountExternalKey='{}'", paymentStateContext.getAccount().getExternalKey(), originalExceptionOrCause);
         }
         return convertToUnknownTransactionStatusAndErroredPaymentState(originalExceptionOrCause);
-
     }
 
     //
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
index 2c849ca..2053ba1 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
@@ -444,11 +444,11 @@ public class DefaultPaymentDao implements PaymentDao {
     }
 
     @Override
-    public List<PaymentMethodModelDao> getPaymentMethods(final UUID accountId, final InternalTenantContext context) {
+    public List<PaymentMethodModelDao> getPaymentMethods(final InternalTenantContext context) {
         return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<PaymentMethodModelDao>>() {
             @Override
             public List<PaymentMethodModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
-                return entitySqlDaoWrapperFactory.become(PaymentMethodSqlDao.class).getByAccountId(accountId.toString(), context);
+                return entitySqlDaoWrapperFactory.become(PaymentMethodSqlDao.class).getForAccount(context);
             }
         });
     }
@@ -513,8 +513,7 @@ public class DefaultPaymentDao implements PaymentDao {
     }
 
     @Override
-    public List<PaymentMethodModelDao> refreshPaymentMethods(final UUID accountId, final String pluginName,
-                                                             final List<PaymentMethodModelDao> newPaymentMethods, final InternalCallContext context) {
+    public List<PaymentMethodModelDao> refreshPaymentMethods(final String pluginName, final List<PaymentMethodModelDao> newPaymentMethods, final InternalCallContext context) {
         return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<PaymentMethodModelDao>>() {
 
             @Override
@@ -523,7 +522,7 @@ public class DefaultPaymentDao implements PaymentDao {
                 // Look at all payment methods, including deleted ones. We assume that newPaymentMethods (payment methods returned by the plugin)
                 // is the full set of non-deleted payment methods in the plugin. If a payment method was marked as deleted on our side,
                 // but is still existing in the plugin, we will un-delete it.
-                final List<PaymentMethodModelDao> allPaymentMethodsForAccount = transactional.getByAccountIdIncludedDelete(accountId.toString(), context);
+                final List<PaymentMethodModelDao> allPaymentMethodsForAccount = transactional.getForAccountIncludedDelete(context);
 
                 // Consider only the payment methods for the plugin we are refreshing
                 final Collection<PaymentMethodModelDao> existingPaymentMethods = Collections2.filter(allPaymentMethodsForAccount,
@@ -567,7 +566,7 @@ public class DefaultPaymentDao implements PaymentDao {
                         deletedPaymentMethodInTransaction(entitySqlDaoWrapperFactory, existingPaymentMethod.getId(), context);
                     }
                 }
-                return transactional.getByAccountId(accountId.toString(), context);
+                return transactional.getForAccount(context);
             }
         });
     }
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
index 69b6941..4c3eeee 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
@@ -84,7 +84,7 @@ public interface PaymentDao {
 
     public PaymentMethodModelDao getPaymentMethodByExternalKeyIncludedDeleted(String paymentMethodExternalKey, InternalTenantContext context);
 
-    public List<PaymentMethodModelDao> getPaymentMethods(UUID accountId, InternalTenantContext context);
+    public List<PaymentMethodModelDao> getPaymentMethods(InternalTenantContext context);
 
     public Pagination<PaymentMethodModelDao> getPaymentMethods(String pluginName, Long offset, Long limit, InternalTenantContext context);
 
@@ -92,5 +92,5 @@ public interface PaymentDao {
 
     public void deletedPaymentMethod(UUID paymentMethodId, InternalCallContext context);
 
-    public List<PaymentMethodModelDao> refreshPaymentMethods(UUID accountId, String pluginName, List<PaymentMethodModelDao> paymentMethods, InternalCallContext context);
+    public List<PaymentMethodModelDao> refreshPaymentMethods(String pluginName, List<PaymentMethodModelDao> paymentMethods, InternalCallContext context);
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentMethodSqlDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentMethodSqlDao.java
index 0db4703..ec7cc3d 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentMethodSqlDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentMethodSqlDao.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -57,10 +59,10 @@ public interface PaymentMethodSqlDao extends EntitySqlDao<PaymentMethodModelDao,
                                                          @BindBean final InternalTenantContext context);
 
     @SqlQuery
-    List<PaymentMethodModelDao> getByAccountId(@Bind("accountId") final String accountId, @BindBean final InternalTenantContext context);
+    List<PaymentMethodModelDao> getForAccount(@BindBean final InternalTenantContext context);
 
     @SqlQuery
-    List<PaymentMethodModelDao> getByAccountIdIncludedDelete(@Bind("accountId") final String accountId, @BindBean final InternalTenantContext context);
+    List<PaymentMethodModelDao> getForAccountIncludedDelete(@BindBean final InternalTenantContext context);
 
     @SqlQuery
     @SmartFetchSize(shouldStream = true)
diff --git a/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java b/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
index d8012d2..01b15f1 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
@@ -17,6 +17,7 @@
 
 package org.killbill.billing.payment.dispatcher;
 
+import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.Callable;
 
@@ -26,6 +27,7 @@ import org.apache.shiro.util.ThreadContext;
 import org.killbill.billing.util.UUIDs;
 import org.killbill.commons.request.Request;
 import org.killbill.commons.request.RequestData;
+import org.slf4j.MDC;
 
 public class CallableWithRequestData<T> implements Callable<T> {
 
@@ -33,13 +35,20 @@ public class CallableWithRequestData<T> implements Callable<T> {
     private final Random random;
     private final SecurityManager securityManager;
     private final Subject subject;
+    private final Map<String, String> mdcContextMap;
     private final Callable<T> delegate;
 
-    public CallableWithRequestData(final RequestData requestData, final Random random, final SecurityManager securityManager, final Subject subject, final Callable<T> delegate) {
+    public CallableWithRequestData(final RequestData requestData,
+                                   final Random random,
+                                   final SecurityManager securityManager,
+                                   final Subject subject,
+                                   final Map<String, String> mdcContextMap,
+                                   final Callable<T> delegate) {
         this.requestData = requestData;
         this.random = random;
         this.securityManager = securityManager;
         this.subject = subject;
+        this.mdcContextMap = mdcContextMap;
         this.delegate = delegate;
     }
 
@@ -50,12 +59,14 @@ public class CallableWithRequestData<T> implements Callable<T> {
             UUIDs.setRandom(random);
             ThreadContext.bind(securityManager);
             ThreadContext.bind(subject);
+            MDC.setContextMap(mdcContextMap);
             return delegate.call();
         } finally {
             Request.resetPerThreadRequestData();
             UUIDs.setRandom(null);
             ThreadContext.unbindSecurityManager();
             ThreadContext.unbindSubject();
+            MDC.clear();
         }
     }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/dispatcher/PaymentPluginDispatcher.java b/payment/src/main/java/org/killbill/billing/payment/dispatcher/PaymentPluginDispatcher.java
index 8454892..fec3c2e 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dispatcher/PaymentPluginDispatcher.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dispatcher/PaymentPluginDispatcher.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -48,20 +48,20 @@ public class PaymentPluginDispatcher {
             log.debug("Successful plugin(s) call of {} for account {} with result {}", pluginNames, accountId, result);
             return result;
         } catch (final TimeoutException e) {
-            final String errorMessage = String.format("TimeoutException while executing the plugin(s) %s", pluginNames);
-            log.warn(errorMessage, e);
+            final String errorMessage = String.format("TimeoutException while executing plugin='%s'", pluginNames);
+            log.warn(errorMessage);
             throw new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_TIMEOUT, accountId, errorMessage);
         } catch (final InterruptedException e) {
             Thread.currentThread().interrupt();
-            final String errorMessage = String.format("InterruptedException while executing the following plugin(s): %s", pluginNames);
+            final String errorMessage = String.format("InterruptedException while executing plugin='%s'", pluginNames);
             log.warn(errorMessage, e);
             throw new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, MoreObjects.firstNonNull(e.getMessage(), errorMessage));
         } catch (final ExecutionException e) {
             if (e.getCause() instanceof PaymentApiException) {
                 throw (PaymentApiException) e.getCause();
             } else if (e.getCause() instanceof LockFailedException) {
-                final String format = String.format("Failed to lock account %s", accountExternalKey);
-                log.error(format, e);
+                final String format = String.format("Failed to lock accountExternalKey='%s'", accountExternalKey);
+                log.warn(format);
                 throw new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, format);
             } else {
                 throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, MoreObjects.firstNonNull(e.getMessage(), ""));
diff --git a/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java b/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java
index fa65e1b..c31bbbe 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java
@@ -31,6 +31,7 @@ import org.killbill.billing.util.UUIDs;
 import org.killbill.commons.profiling.Profiling;
 import org.killbill.commons.profiling.ProfilingData;
 import org.killbill.commons.request.Request;
+import org.slf4j.MDC;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -62,6 +63,7 @@ public class PluginDispatcher<ReturnType> {
                                                                                                                      UUIDs.getRandom(),
                                                                                                                      ThreadContext.getSecurityManager(),
                                                                                                                      ThreadContext.getSubject(),
+                                                                                                                     MDC.getCopyOfContextMap(),
                                                                                                                      task);
 
         final Future<PluginDispatcherReturnType<ReturnType>> future = pluginExecutor.submit(callableWithRequestData);
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java
index 93603b1..e27a8b2 100644
--- a/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java
@@ -77,7 +77,7 @@ public class DefaultPaymentService implements PaymentService {
             eventBus.register(paymentBusEventHandler);
             eventBus.register(tagHandler);
         } catch (final PersistentBus.EventBusException e) {
-            log.error("Unable to register with the EventBus!", e);
+            log.error("Failed to register bus handlers", e);
         }
         paymentExecutors.initialize();
         retryService.initialize();
@@ -96,7 +96,7 @@ public class DefaultPaymentService implements PaymentService {
             eventBus.unregister(paymentBusEventHandler);
             eventBus.unregister(tagHandler);
         } catch (final PersistentBus.EventBusException e) {
-            throw new RuntimeException("Unable to unregister to the EventBus!", e);
+            throw new RuntimeException("Failed to unregister bus handlers", e);
         }
         retryService.stop();
         janitor.stop();
diff --git a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
index 7ccaba6..1bf13ea 100644
--- a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
@@ -46,6 +46,7 @@ import org.killbill.billing.invoice.api.InvoiceApiException;
 import org.killbill.billing.invoice.api.InvoiceInternalApi;
 import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentType;
 import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.payment.api.TransactionStatus;
@@ -154,24 +155,25 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
                     existingInvoicePayment = invoiceApi.getInvoicePaymentForAttempt(paymentControlContext.getPaymentId(), internalContext);
                     if (existingInvoicePayment != null && existingInvoicePayment.isSuccess()) {
                         // Only one successful purchase per payment (the invoice could be linked to multiple successful payments though)
-                        log.info("onSuccessCall was already completed for payment purchase: " + paymentControlContext.getPaymentId());
+                        log.info("onSuccessCall was already completed for purchase paymentId='{}'", paymentControlContext.getPaymentId());
                     } else {
                         final BigDecimal invoicePaymentAmount;
                         if (paymentControlContext.getCurrency() == paymentControlContext.getProcessedCurrency()) {
                             invoicePaymentAmount = paymentControlContext.getProcessedAmount();
                         } else {
-                            log.warn("Currency {} of invoice payment {} doesn't match invoice currency {}, assuming it is a full payment" , paymentControlContext.getProcessedCurrency(), paymentControlContext.getPaymentId(), paymentControlContext.getCurrency());
+                            log.warn("processedCurrency='{}' of invoice paymentId='{}' doesn't match invoice currency='{}', assuming it is a full payment" , paymentControlContext.getProcessedCurrency(), paymentControlContext.getPaymentId(), paymentControlContext.getCurrency());
                             invoicePaymentAmount = paymentControlContext.getAmount();
                         }
-                        log.debug("Notifying invoice of successful payment: id={}, amount={}, currency={}, invoiceId={}", paymentControlContext.getPaymentId(), invoicePaymentAmount, paymentControlContext.getCurrency(), invoiceId);
-                        invoiceApi.notifyOfPayment(invoiceId,
-                                                   invoicePaymentAmount,
-                                                   paymentControlContext.getCurrency(),
-                                                   paymentControlContext.getProcessedCurrency(),
-                                                   paymentControlContext.getPaymentId(),
-                                                   paymentControlContext.getCreatedDate(),
-                                                   true,
-                                                   internalContext);
+                        log.debug("Notifying invoice of successful paymentId='{}', amount='{}', currency='{}', invoiceId='{}'", paymentControlContext.getPaymentId(), invoicePaymentAmount, paymentControlContext.getCurrency(), invoiceId);
+                        invoiceApi.recordPaymentAttemptCompletion(invoiceId,
+                                                                  invoicePaymentAmount,
+                                                                  paymentControlContext.getCurrency(),
+                                                                  paymentControlContext.getProcessedCurrency(),
+                                                                  paymentControlContext.getPaymentId(),
+                                                                  paymentControlContext.getTransactionExternalKey(),
+                                                                  paymentControlContext.getCreatedDate(),
+                                                                  true,
+                                                                  internalContext);
                     }
                     break;
 
@@ -179,14 +181,14 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
                     final Map<UUID, BigDecimal> idWithAmount = extractIdsWithAmountFromProperties(pluginProperties);
                     final PluginProperty prop = getPluginProperty(pluginProperties, PROP_IPCD_REFUND_WITH_ADJUSTMENTS);
                     final boolean isAdjusted = prop != null ? Boolean.valueOf((String) prop.getValue()) : false;
-                    invoiceApi.createRefund(paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), isAdjusted, idWithAmount, paymentControlContext.getTransactionExternalKey(), internalContext);
+                    invoiceApi.recordRefund(paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), isAdjusted, idWithAmount, paymentControlContext.getTransactionExternalKey(), internalContext);
                     break;
 
                 case CHARGEBACK:
                     existingInvoicePayment = invoiceApi.getInvoicePaymentForChargeback(paymentControlContext.getPaymentId(), internalContext);
                     if (existingInvoicePayment != null) {
                         // We don't support partial chargebacks (yet?)
-                        log.info("onSuccessCall was already completed for payment chargeback: " + paymentControlContext.getPaymentId());
+                        log.info("onSuccessCall was already completed for chargeback paymentId='{}'", paymentControlContext.getPaymentId());
                     } else {
                         final InvoicePayment linkedInvoicePayment = invoiceApi.getInvoicePaymentForAttempt(paymentControlContext.getPaymentId(), internalContext);
 
@@ -203,7 +205,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
                             currency = linkedInvoicePayment.getCurrency();
                         }
 
-                        invoiceApi.createChargeback(paymentControlContext.getPaymentId(), amount, currency, internalContext);
+                        invoiceApi.recordChargeback(paymentControlContext.getPaymentId(), amount, currency, internalContext);
                     }
                     break;
 
@@ -211,7 +213,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
                     throw new IllegalStateException("Unexpected transactionType " + transactionType);
             }
         } catch (final InvoiceApiException e) {
-            log.error("InvoicePaymentControlPluginApi onSuccessCall failed for attemptId = " + paymentControlContext.getAttemptPaymentId() + ", transactionType  = " + transactionType, e);
+            log.warn("onSuccessCall failed for attemptId='{}', transactionType='{}'", paymentControlContext.getAttemptPaymentId(), transactionType, e);
         }
 
         return new DefaultOnSuccessPaymentControlResult();
@@ -228,15 +230,16 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
                 final UUID invoiceId = getInvoiceId(pluginProperties);
                 try {
                     log.debug("Notifying invoice of failed payment: id={}, amount={}, currency={}, invoiceId={}", paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), paymentControlContext.getCurrency(), invoiceId);
-                    invoiceApi.notifyOfPayment(invoiceId,
-                                               BigDecimal.ZERO,
-                                               paymentControlContext.getCurrency(),
-                                               // processed currency may be null so we use currency; processed currency will be updated if/when payment succeeds
-                                               paymentControlContext.getCurrency(),
-                                               paymentControlContext.getPaymentId(),
-                                               paymentControlContext.getCreatedDate(),
-                                               false,
-                                               internalContext);
+                    invoiceApi.recordPaymentAttemptCompletion(invoiceId,
+                                                              BigDecimal.ZERO,
+                                                              paymentControlContext.getCurrency(),
+                                                              // processed currency may be null so we use currency; processed currency will be updated if/when payment succeeds
+                                                              paymentControlContext.getCurrency(),
+                                                              paymentControlContext.getPaymentId(),
+                                                              paymentControlContext.getTransactionExternalKey(),
+                                                              paymentControlContext.getCreatedDate(),
+                                                              false,
+                                                              internalContext);
                 } catch (final InvoiceApiException e) {
                     log.error("InvoicePaymentControlPluginApi onFailureCall failed ton update invoice for attemptId = " + paymentControlContext.getAttemptPaymentId() + ", transactionType  = " + transactionType, e);
                 }
@@ -275,7 +278,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
     private PriorPaymentControlResult getPluginPurchaseResult(final PaymentControlContext paymentControlPluginContext, final Iterable<PluginProperty> pluginProperties, final InternalCallContext internalContext) throws PaymentControlApiException {
         try {
             final UUID invoiceId = getInvoiceId(pluginProperties);
-            final Invoice invoice = rebalanceAndGetInvoice(invoiceId, internalContext);
+            final Invoice invoice = getAndSanitizeInvoice(invoiceId, internalContext);
             final BigDecimal requestedAmount = validateAndComputePaymentAmount(invoice, paymentControlPluginContext.getAmount(), paymentControlPluginContext.isApiPayment());
 
             final boolean isAborted = requestedAmount.compareTo(BigDecimal.ZERO) == 0;
@@ -285,11 +288,27 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
 
             if (paymentControlPluginContext.isApiPayment() && isAborted) {
                 throw new PaymentControlApiException("Abort purchase call: ", new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_EXCEPTION,
-                                                                                                     String.format("Payment for invoice %s aborted : invoice balance is = %s, requested payment amount is = %s",
+                                                                                                     String.format("Aborted Payment for invoice %s : invoice balance is = %s, requested payment amount is = %s",
                                                                                                                    invoice.getId(),
                                                                                                                    invoice.getBalance(),
                                                                                                                    paymentControlPluginContext.getAmount())));
             } else {
+
+                //
+                // Insert attempt row with a success = false status to implement a two-phase commit strategy and guard against scenario where payment would go through
+                // but onSuccessCall callback never gets called (leaving the place for a double payment if user retries the operation)
+                //
+                invoiceApi.recordPaymentAttemptInit(invoice.getId(),
+                                                    BigDecimal.ZERO,
+                                                    paymentControlPluginContext.getCurrency(),
+                                                    paymentControlPluginContext.getCurrency(),
+                                                    // Likely to be null, but we don't care as we use the transactionExternalKey
+                                                    // to match the operation in the checkForIncompleteInvoicePaymentAndRepair logic below
+                                                    paymentControlPluginContext.getPaymentId(),
+                                                    paymentControlPluginContext.getTransactionExternalKey(),
+                                                    paymentControlPluginContext.getCreatedDate(),
+                                                    internalContext);
+
                 return new DefaultPriorPaymentControlResult(isAborted, requestedAmount);
             }
         } catch (final InvoiceApiException e) {
@@ -493,25 +512,76 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
         }));
     }
 
-    private Invoice rebalanceAndGetInvoice(final UUID invoiceId, final InternalCallContext context) throws InvoiceApiException {
+    private Invoice getAndSanitizeInvoice(final UUID invoiceId, final InternalCallContext context) throws InvoiceApiException {
         final Invoice invoicePriorRebalancing = invoiceApi.getInvoiceById(invoiceId, context);
         invoiceApi.consumeExistingCBAOnAccountWithUnpaidInvoices(invoicePriorRebalancing.getAccountId(), context);
         final Invoice invoice = invoiceApi.getInvoiceById(invoiceId, context);
-        return invoice;
+
+        if (checkForIncompleteInvoicePaymentAndRepair(invoice, context)) {
+            // Fetch new repaired 'invoice'
+            return invoiceApi.getInvoiceById(invoiceId, context);
+        } else {
+            return invoice;
+        }
+    }
+
+    private boolean checkForIncompleteInvoicePaymentAndRepair(final Invoice invoice, final InternalCallContext internalContext) throws InvoiceApiException {
+
+        final List<InvoicePayment> invoicePayments = invoice.getPayments();
+
+        // Look for ATTEMPT matching that invoiceId that are not successful and extract matching paymentTransaction
+        final InvoicePayment incompleteInvoicePayment = Iterables.tryFind(invoicePayments, new Predicate<InvoicePayment>() {
+            @Override
+            public boolean apply(final InvoicePayment input) {
+                return input.getType() == InvoicePaymentType.ATTEMPT && !input.isSuccess();
+            }
+        }).orNull();
+
+        // If such (incomplete) paymentTransaction exists, verify the state of the payment transaction
+        if (incompleteInvoicePayment != null) {
+            final String transactionExternalKey = incompleteInvoicePayment.getPaymentCookieId();
+            final List<PaymentTransactionModelDao> transactions = paymentDao.getPaymentTransactionsByExternalKey(transactionExternalKey, internalContext);
+            final PaymentTransactionModelDao successfulTransaction = Iterables.tryFind(transactions, new Predicate<PaymentTransactionModelDao>() {
+                @Override
+                public boolean apply(final PaymentTransactionModelDao input) {
+                    //
+                    // In reality this is more tricky because the matching transaction could be an UNKNOWN or PENDING (unsupported by the plugin) state
+                    // In case of UNKNOWN, we don't know what to do: fixing it could result in not paying, and not fixing it could result in double payment
+                    // Current code ignores it, which means we might end up in doing a double payment in that very edgy scenario, and customer would have to request a refund.
+                    //
+                    return input.getTransactionStatus() == TransactionStatus.SUCCESS;
+                }
+            }).orNull();
+
+            if (successfulTransaction != null) {
+                log.info(String.format("Detected an incomplete invoicePayment row for invoiceId='%s' and transactionExternalKey='%s', will correct status", invoice.getId(), successfulTransaction.getTransactionExternalKey()));
+
+                invoiceApi.recordPaymentAttemptCompletion(invoice.getId(),
+                                                          successfulTransaction.getAmount(),
+                                                          successfulTransaction.getCurrency(),
+                                                          successfulTransaction.getProcessedCurrency(),
+                                                          successfulTransaction.getPaymentId(),
+                                                          successfulTransaction.getTransactionExternalKey(),
+                                                          successfulTransaction.getCreatedDate(),
+                                                          true,
+                                                          internalContext);
+                return true;
+
+            }
+        }
+        return false;
     }
 
     private BigDecimal validateAndComputePaymentAmount(final Invoice invoice, @Nullable final BigDecimal inputAmount, final boolean isApiPayment) {
 
         if (invoice.getBalance().compareTo(BigDecimal.ZERO) <= 0) {
-            log.info("Invoice " + invoice.getId() + " has already been paid");
+            log.info("invoiceId='{}' has already been paid", invoice.getId());
             return BigDecimal.ZERO;
         }
         if (isApiPayment &&
             inputAmount != null &&
             invoice.getBalance().compareTo(inputAmount) < 0) {
-            log.info("Invoice " + invoice.getId() +
-                     " has a balance of " + invoice.getBalance().floatValue() +
-                     " less than retry payment amount of " + inputAmount.floatValue());
+            log.info("invoiceId='{}' has a balance='{}' < retry paymentAmount='{}'", invoice.getId(), invoice.getBalance().floatValue(), inputAmount.floatValue());
             return BigDecimal.ZERO;
         }
         if (inputAmount == null) {
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentControlProviderPluginRegistry.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentControlProviderPluginRegistry.java
index 40a3ad5..9ebbb2e 100644
--- a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentControlProviderPluginRegistry.java
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentControlProviderPluginRegistry.java
@@ -41,13 +41,13 @@ public class DefaultPaymentControlProviderPluginRegistry implements OSGIServiceR
 
     @Override
     public void registerService(final OSGIServiceDescriptor desc, final PaymentControlPluginApi service) {
-        log.info("DefaultPaymentControlProviderPluginRegistry registering service " + desc.getRegistrationName());
+        log.info("Registering service='{}'", desc.getRegistrationName());
         pluginsByName.put(desc.getRegistrationName(), service);
     }
 
     @Override
     public void unregisterService(final String serviceName) {
-        log.info("DefaultPaymentControlProviderPluginRegistry unregistering service " + serviceName);
+        log.info("Unregistering service='{}'", serviceName);
         pluginsByName.remove(serviceName);
     }
 
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java
index 5d9c44e..ce8d315 100644
--- a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java
@@ -45,13 +45,13 @@ public class DefaultPaymentProviderPluginRegistry implements OSGIServiceRegistra
 
     @Override
     public void registerService(final OSGIServiceDescriptor desc, final PaymentPluginApi service) {
-        log.info("DefaultPaymentProviderPluginRegistry registering service " + desc.getRegistrationName());
+        log.info("Registering service='{}'", desc.getRegistrationName());
         pluginsByName.put(desc.getRegistrationName(), service);
     }
 
     @Override
     public void unregisterService(final String serviceName) {
-        log.info("DefaultPaymentProviderPluginRegistry unregistering service " + serviceName);
+        log.info("Unregistering service='{}'", serviceName);
         pluginsByName.remove(serviceName);
     }
 
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/BaseRetryService.java b/payment/src/main/java/org/killbill/billing/payment/retry/BaseRetryService.java
index 7757b65..ded591c 100644
--- a/payment/src/main/java/org/killbill/billing/payment/retry/BaseRetryService.java
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/BaseRetryService.java
@@ -124,10 +124,10 @@ public abstract class BaseRetryService implements RetryService {
                     }
                 }
             } catch (final NoSuchNotificationQueue e) {
-                log.error(String.format("Failed to retrieve notification queue %s:%s", DefaultPaymentService.SERVICE_NAME, getQueueName()));
+                log.error("Failed to retrieve notification queue='{}', service='{}'", getQueueName(), DefaultPaymentService.SERVICE_NAME);
                 return false;
             } catch (final IOException e) {
-                log.error(String.format("Failed to serialize notificationQueue event for objectId %s", objectId));
+                log.error("Failed to serialize notificationQueue event for objectId='{}'", objectId);
                 return false;
             }
             return true;
diff --git a/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentMethodSqlDao.sql.stg b/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentMethodSqlDao.sql.stg
index aeccb57..7d2d59c 100644
--- a/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentMethodSqlDao.sql.stg
+++ b/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentMethodSqlDao.sql.stg
@@ -75,21 +75,21 @@ where id = :id
 ;
 >>
 
-getByAccountId(accountId) ::= <<
+getForAccount() ::= <<
 select
 <allTableFields()>
 from <tableName()>
-where account_id = :accountId
+where <accountRecordIdField()> = :accountRecordId
 <andCheckSoftDeletionWithComma()>
 <AND_CHECK_TENANT()>
 ;
 >>
 
-getByAccountIdIncludedDelete(accountId) ::= <<
+getForAccountIncludedDelete() ::= <<
 select
 <allTableFields()>
 from <tableName()>
-where account_id = :accountId
+where <accountRecordIdField()> = :accountRecordId
 <AND_CHECK_TENANT()>
 ;
 >>
diff --git a/payment/src/main/resources/org/killbill/billing/payment/ddl.sql b/payment/src/main/resources/org/killbill/billing/payment/ddl.sql
index 98b3c0a..fef6bd7 100644
--- a/payment/src/main/resources/org/killbill/billing/payment/ddl.sql
+++ b/payment/src/main/resources/org/killbill/billing/payment/ddl.sql
@@ -77,7 +77,6 @@ CREATE TABLE payment_methods (
 CREATE UNIQUE INDEX payment_methods_id ON payment_methods(id);
 CREATE UNIQUE INDEX payment_methods_external_key ON payment_methods(external_key, tenant_record_id);
 CREATE INDEX payment_methods_plugin_name ON payment_methods(plugin_name);
-CREATE INDEX payment_methods_active_accnt ON payment_methods(is_active, account_id);
 CREATE INDEX payment_methods_tenant_account_record_id ON payment_methods(tenant_record_id, account_record_id);
 
 DROP TABLE IF EXISTS payment_method_history;
diff --git a/payment/src/main/resources/org/killbill/billing/payment/migration/V20160324060345__revisit_payment_methods_indexes_509.sql b/payment/src/main/resources/org/killbill/billing/payment/migration/V20160324060345__revisit_payment_methods_indexes_509.sql
new file mode 100644
index 0000000..3b74b7a
--- /dev/null
+++ b/payment/src/main/resources/org/killbill/billing/payment/migration/V20160324060345__revisit_payment_methods_indexes_509.sql
@@ -0,0 +1 @@
+drop index payment_methods_active_accnt on payment_methods;
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 14146d6..7453caf 100644
--- a/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
+++ b/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
@@ -456,12 +456,6 @@
         <linkStateMachine>
             <initialStateMachine>AUTHORIZE</initialStateMachine>
             <initialState>AUTH_SUCCESS</initialState>
-            <finalStateMachine>AUTHORIZE</finalStateMachine>
-            <finalState>AUTH_INIT</finalState>
-        </linkStateMachine>
-        <linkStateMachine>
-            <initialStateMachine>AUTHORIZE</initialStateMachine>
-            <initialState>AUTH_SUCCESS</initialState>
             <finalStateMachine>CAPTURE</finalStateMachine>
             <finalState>CAPTURE_INIT</finalState>
         </linkStateMachine>
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 a3cca2f..bb8c247 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
@@ -1099,74 +1099,6 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
         }
     }
 
-    @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/371")
-    public void testApiWithDuplicatePendingPaymentTransaction() throws Exception {
-        final BigDecimal requestedAmount = BigDecimal.TEN;
-
-        for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
-            final String payment1ExternalKey = UUID.randomUUID().toString();
-            final String payment1TransactionExternalKey = UUID.randomUUID().toString();
-            final String payment2ExternalKey = UUID.randomUUID().toString();
-            final String payment2TransactionExternalKey = UUID.randomUUID().toString();
-            final String payment3TransactionExternalKey = UUID.randomUUID().toString();
-
-            final Payment pendingPayment1 = createPayment(transactionType, null, payment1ExternalKey, payment1TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
-            assertNotNull(pendingPayment1);
-            Assert.assertEquals(pendingPayment1.getExternalKey(), payment1ExternalKey);
-            Assert.assertEquals(pendingPayment1.getTransactions().size(), 1);
-            Assert.assertEquals(pendingPayment1.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment1.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment1.getTransactions().get(0).getCurrency(), account.getCurrency());
-            Assert.assertEquals(pendingPayment1.getTransactions().get(0).getExternalKey(), payment1TransactionExternalKey);
-            Assert.assertEquals(pendingPayment1.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
-
-            // Attempt to create a second transaction for the same payment, but with a different transaction external key
-            final Payment pendingPayment2 = createPayment(transactionType, null, payment1ExternalKey, payment2TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
-            assertNotNull(pendingPayment2);
-            Assert.assertEquals(pendingPayment2.getId(), pendingPayment1.getId());
-            Assert.assertEquals(pendingPayment2.getExternalKey(), payment1ExternalKey);
-            Assert.assertEquals(pendingPayment2.getTransactions().size(), 2);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(0).getCurrency(), account.getCurrency());
-            Assert.assertEquals(pendingPayment2.getTransactions().get(0).getExternalKey(), payment1TransactionExternalKey);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(1).getAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(1).getProcessedAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(1).getCurrency(), account.getCurrency());
-            Assert.assertEquals(pendingPayment2.getTransactions().get(1).getExternalKey(), payment2TransactionExternalKey);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
-
-            try {
-                // Verify we cannot use the same transaction external key on a different payment if the payment id isn't specified
-                createPayment(transactionType, null, payment2ExternalKey, payment1TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
-                Assert.fail();
-            } catch (final PaymentApiException e) {
-                Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
-            }
-
-            try {
-                // Verify we cannot use the same transaction external key on a different payment if the payment id isn't specified
-                createPayment(transactionType, null, payment2ExternalKey, payment2TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
-                Assert.fail();
-            } catch (final PaymentApiException e) {
-                Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
-            }
-
-            // Attempt to create a second transaction for a different payment
-            final Payment pendingPayment3 = createPayment(transactionType, null, payment2ExternalKey, payment3TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
-            assertNotNull(pendingPayment3);
-            Assert.assertNotEquals(pendingPayment3.getId(), pendingPayment1.getId());
-            Assert.assertEquals(pendingPayment3.getExternalKey(), payment2ExternalKey);
-            Assert.assertEquals(pendingPayment3.getTransactions().size(), 1);
-            Assert.assertEquals(pendingPayment3.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment3.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment3.getTransactions().get(0).getCurrency(), account.getCurrency());
-            Assert.assertEquals(pendingPayment3.getTransactions().get(0).getExternalKey(), payment3TransactionExternalKey);
-            Assert.assertEquals(pendingPayment3.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
-        }
-    }
-
     @Test(groups = "slow")
     public void testApiWithPendingPaymentTransaction() throws Exception {
         for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
@@ -1270,11 +1202,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
 
                 assertNotNull(thrownException);
 
-                Throwable operationException = thrownException.getCause();
-                assertNotNull(operationException);
-                assertTrue(operationException instanceof OperationException);
-
-                Throwable timeoutException = operationException.getCause();
+                Throwable timeoutException = thrownException.getCause();
                 assertNotNull(timeoutException);
                 assertTrue(timeoutException instanceof TimeoutException);
 
@@ -1286,6 +1214,147 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
         assertTrue(spyLogger.contains("TimeoutException.*" + pluginName, Optional.of(SpyLogger.LOG_LEVEL_WARN)));
     }
 
+
+    @Test(groups = "slow")
+    public void testSanityAcrossTransactionTypes() throws PaymentApiException {
+
+        final BigDecimal requestedAmount = BigDecimal.TEN;
+        final String paymentExternalKey = "ahhhhhhhh";
+        final String transactionExternalKey = "okkkkkkk";
+
+        final Payment pendingPayment = createPayment(TransactionType.AUTHORIZE, null, paymentExternalKey, transactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
+        assertNotNull(pendingPayment);
+        Assert.assertEquals(pendingPayment.getExternalKey(), paymentExternalKey);
+        Assert.assertEquals(pendingPayment.getTransactions().size(), 1);
+        Assert.assertEquals(pendingPayment.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0);
+        Assert.assertEquals(pendingPayment.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0);
+        Assert.assertEquals(pendingPayment.getTransactions().get(0).getCurrency(), account.getCurrency());
+        Assert.assertEquals(pendingPayment.getTransactions().get(0).getExternalKey(), transactionExternalKey);
+        Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
+
+
+        try {
+            createPayment(TransactionType.PURCHASE, null, paymentExternalKey, transactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
+            Assert.fail("PURCHASE transaction with same key should have failed");
+        } catch (final PaymentApiException expected) {
+            Assert.assertEquals(expected.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
+        }
+    }
+
+    @Test(groups = "slow")
+    public void testSuccessfulInitialTransactionToSameTransaction() throws Exception {
+
+        final BigDecimal requestedAmount = BigDecimal.TEN;
+        for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
+
+            final String paymentExternalKey = UUID.randomUUID().toString();
+            final String keyA = UUID.randomUUID().toString();
+
+            final Payment processedPayment = createPayment(transactionType, null, paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.PROCESSED);
+            assertNotNull(processedPayment);
+            Assert.assertEquals(processedPayment.getTransactions().size(), 1);
+            Assert.assertEquals(processedPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+            // Attempt to create another {AUTH, PURCHASE, CREDIT} with different key => KB state machine should make the request fail as we don't allow
+            // multiple SUCCESS {AUTH, PURCHASE, CREDIT}
+            final String keyB = UUID.randomUUID().toString();
+            try {
+                createPayment(transactionType, processedPayment.getId(), paymentExternalKey, keyB, requestedAmount, PaymentPluginStatus.PROCESSED);
+                Assert.fail("Retrying initial successful transaction (AUTHORIZE, PURCHASE, CREDIT) with same different key should fail");
+            } catch (final PaymentApiException e) {
+            }
+
+            // Attempt to create another {AUTH, PURCHASE, CREDIT} with same key => key constraint should make the request fail
+            try {
+                createPayment(transactionType, processedPayment.getId(), paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.PROCESSED);
+                Assert.fail("Retrying initial successful transaction (AUTHORIZE, PURCHASE, CREDIT) with same transaction key should fail");
+            } catch (final PaymentApiException e) {
+            }
+        }
+    }
+
+
+    @Test(groups = "slow")
+    public void testPendingInitialTransactionToSameTransaction() throws Exception {
+
+        final BigDecimal requestedAmount = BigDecimal.TEN;
+        for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
+
+            final String paymentExternalKey = UUID.randomUUID().toString();
+            final String keyA = UUID.randomUUID().toString();
+
+            final Payment pendingPayment = createPayment(transactionType, null, paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.PENDING);
+            assertNotNull(pendingPayment);
+            Assert.assertEquals(pendingPayment.getTransactions().size(), 1);
+            Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
+
+            // Attempt to create another {AUTH, PURCHASE, CREDIT} with different key => KB state machine should make the request fail as we don't allow
+            // multiple SUCCESS {AUTH, PURCHASE, CREDIT}
+            final String keyB = UUID.randomUUID().toString();
+            try {
+                createPayment(transactionType, pendingPayment.getId(), paymentExternalKey, keyB, requestedAmount, PaymentPluginStatus.PROCESSED);
+                Assert.fail("Retrying initial successful transaction (AUTHORIZE, PURCHASE, CREDIT) with same different key should fail");
+            } catch (final PaymentApiException e) {
+            }
+
+            // Attempt to create another {AUTH, PURCHASE, CREDIT} with same key => That should work because we are completing the payment
+            final Payment completedPayment = createPayment(transactionType, pendingPayment.getId(), paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.PROCESSED);
+            assertNotNull(completedPayment);
+            Assert.assertEquals(completedPayment.getId(), pendingPayment.getId());
+            Assert.assertEquals(completedPayment.getTransactions().size(), 1);
+        }
+    }
+
+
+    @Test(groups = "slow")
+    public void testFailedInitialTransactionToSameTransactionWithSameKey() throws Exception {
+
+        final BigDecimal requestedAmount = BigDecimal.TEN;
+        for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
+
+            final String paymentExternalKey = UUID.randomUUID().toString();
+            final String keyA = UUID.randomUUID().toString();
+
+            final Payment errorPayment = createPayment(transactionType, null, paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.ERROR);
+            assertNotNull(errorPayment);
+            Assert.assertEquals(errorPayment.getTransactions().size(), 1);
+            Assert.assertEquals(errorPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+
+            // Attempt to create another {AUTH, PURCHASE, CREDIT} with same key => That should work because we are completing the payment
+            final Payment successfulPayment = createPayment(transactionType, errorPayment.getId(), paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.PROCESSED);
+            assertNotNull(successfulPayment);
+            Assert.assertEquals(successfulPayment.getId(), errorPayment.getId());
+            Assert.assertEquals(successfulPayment.getTransactions().size(), 2);
+        }
+    }
+
+
+    @Test(groups = "slow")
+    public void testFailedInitialTransactionToSameTransactionWithDifferentKey() throws Exception {
+
+        final BigDecimal requestedAmount = BigDecimal.TEN;
+        for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
+
+            final String paymentExternalKey = UUID.randomUUID().toString();
+            final String keyA = UUID.randomUUID().toString();
+
+            final Payment errorPayment = createPayment(transactionType, null, paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.ERROR);
+            assertNotNull(errorPayment);
+            Assert.assertEquals(errorPayment.getTransactions().size(), 1);
+            Assert.assertEquals(errorPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+
+            // Attempt to create another {AUTH, PURCHASE, CREDIT} with different key => KB state machine should make the request fail as we don't allow
+            // multiple SUCCESS {AUTH, PURCHASE, CREDIT}
+            final String keyB = UUID.randomUUID().toString();
+            final Payment successfulPayment = createPayment(transactionType, errorPayment.getId(), paymentExternalKey, keyB, requestedAmount, PaymentPluginStatus.PROCESSED);
+            assertNotNull(successfulPayment);
+            Assert.assertEquals(successfulPayment.getId(), errorPayment.getId());
+            Assert.assertEquals(successfulPayment.getTransactions().size(), 2);
+        }
+    }
+
+
+
     private void verifyRefund(final Payment refund, final String paymentExternalKey, final String paymentTransactionExternalKey, final String refundTransactionExternalKey, final BigDecimal requestedAmount, final BigDecimal refundAmount, final TransactionStatus transactionStatus) {
         Assert.assertEquals(refund.getExternalKey(), paymentExternalKey);
         Assert.assertEquals(refund.getTransactions().size(), 2);
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
index 5b74e38..5a50314 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -44,7 +44,6 @@ import org.killbill.billing.payment.core.PaymentProcessor;
 import org.killbill.billing.payment.core.PluginControlPaymentProcessor;
 import org.killbill.billing.payment.core.sm.control.ControlPluginRunner;
 import org.killbill.billing.payment.core.sm.control.PaymentStateControlContext;
-import org.killbill.billing.payment.dao.MockPaymentDao;
 import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
 import org.killbill.billing.payment.dao.PaymentDao;
 import org.killbill.billing.payment.dao.PaymentModelDao;
@@ -157,7 +156,6 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
     @BeforeMethod(groups = "fast")
     public void beforeMethod() throws Exception {
         super.beforeMethod();
-        ((MockPaymentDao) paymentDao).reset();
         this.utcNow = clock.getUTCNow();
 
         runner = new MockRetryablePaymentAutomatonRunner(
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorNoDB.java b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorNoDB.java
index 5da756d..226a34b 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorNoDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorNoDB.java
@@ -59,11 +59,11 @@ public class TestPaymentMethodProcessorNoDB extends PaymentTestSuiteNoDB {
         Mockito.when(account.getId()).thenReturn(accountId);
         Mockito.when(account.getExternalKey()).thenReturn(accountId.toString());
 
-        Assert.assertEquals(paymentMethodProcessor.getPaymentMethods(account.getId(), false, properties, internalCallContext).size(), 0);
+        Assert.assertEquals(paymentMethodProcessor.getPaymentMethods(false, properties, internalCallContext).size(), 0);
 
         // The first call should create the payment method
         final ExternalPaymentProviderPlugin providerPlugin = paymentMethodProcessor.createPaymentMethodAndGetExternalPaymentProviderPlugin(UUID.randomUUID().toString(), account, properties, callContext, internalCallContext);
-        final List<PaymentMethod> paymentMethods = paymentMethodProcessor.getPaymentMethods(account.getId(), false, properties, internalCallContext);
+        final List<PaymentMethod> paymentMethods = paymentMethodProcessor.getPaymentMethods(false, properties, internalCallContext);
         Assert.assertEquals(paymentMethods.size(), 1);
         Assert.assertEquals(paymentMethods.get(0).getPluginName(), ExternalPaymentProviderPlugin.PLUGIN_NAME);
         Assert.assertEquals(paymentMethods.get(0).getAccountId(), account.getId());
@@ -74,7 +74,7 @@ public class TestPaymentMethodProcessorNoDB extends PaymentTestSuiteNoDB {
             final ExternalPaymentProviderPlugin foundProviderPlugin = paymentMethodProcessor.createPaymentMethodAndGetExternalPaymentProviderPlugin(UUID.randomUUID().toString(), account, properties, callContext, internalCallContext);
             Assert.assertNotNull(foundProviderPlugin);
 
-            final List<PaymentMethod> foundPaymentMethods = paymentMethodProcessor.getPaymentMethods(account.getId(), false, properties, internalCallContext);
+            final List<PaymentMethod> foundPaymentMethods = paymentMethodProcessor.getPaymentMethods(false, properties, internalCallContext);
             Assert.assertEquals(foundPaymentMethods.size(), 1);
             Assert.assertEquals(foundPaymentMethods.get(0).getPluginName(), ExternalPaymentProviderPlugin.PLUGIN_NAME);
             Assert.assertEquals(foundPaymentMethods.get(0).getAccountId(), account.getId());
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 f5f71c6..d60f90b 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
@@ -38,6 +38,8 @@ import org.killbill.billing.payment.api.PaymentTransaction;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.payment.api.TransactionStatus;
 import org.killbill.billing.payment.api.TransactionType;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
 import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -71,54 +73,56 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
     public void testClassicFlow() throws Exception {
         final String paymentExternalKey = UUID.randomUUID().toString();
 
+
+        final Iterable<PluginProperty> pluginPropertiesToDriveTransationToPending = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
         // AUTH pre-3DS
         final String authorizationKey = UUID.randomUUID().toString();
         final Payment authorization = paymentProcessor.createAuthorization(true, null, account, null, null, TEN, CURRENCY, paymentExternalKey, authorizationKey,
-                                                                           SHOULD_LOCK_ACCOUNT, PLUGIN_PROPERTIES, callContext, internalCallContext);
-        verifyPayment(authorization, paymentExternalKey, TEN, ZERO, ZERO, 1);
+                                                                           SHOULD_LOCK_ACCOUNT, pluginPropertiesToDriveTransationToPending, callContext, internalCallContext);
+        verifyPayment(authorization, paymentExternalKey, ZERO, ZERO, ZERO, 1);
         final UUID paymentId = authorization.getId();
         verifyPaymentTransaction(authorization.getTransactions().get(0), authorizationKey, TransactionType.AUTHORIZE, TEN, paymentId);
-        paymentBusListener.verify(1, account.getId(), paymentId, TEN);
+        paymentBusListener.verify(1, account.getId(), paymentId, TEN, TransactionStatus.PENDING);
 
         // AUTH post-3DS
-        final String authorizationPost3DSKey = UUID.randomUUID().toString();
-        final Payment authorizationPost3DS = paymentProcessor.createAuthorization(true, null, account, null, paymentId, TEN, CURRENCY, paymentExternalKey, authorizationPost3DSKey,
+        final Payment authorizationPost3DS = paymentProcessor.createAuthorization(true, null, account, null, paymentId, TEN, CURRENCY, paymentExternalKey, authorizationKey,
                                                                                   SHOULD_LOCK_ACCOUNT, PLUGIN_PROPERTIES, callContext, internalCallContext);
-        verifyPayment(authorizationPost3DS, paymentExternalKey, TEN, ZERO, ZERO, 2);
-        verifyPaymentTransaction(authorizationPost3DS.getTransactions().get(1), authorizationPost3DSKey, TransactionType.AUTHORIZE, TEN, paymentId);
-        paymentBusListener.verify(2, account.getId(), paymentId, TEN);
+        verifyPayment(authorizationPost3DS, paymentExternalKey, TEN, ZERO, ZERO, 1);
+        verifyPaymentTransaction(authorizationPost3DS.getTransactions().get(0), authorizationKey, TransactionType.AUTHORIZE, TEN, paymentId);
+        paymentBusListener.verify(2, account.getId(), paymentId, TEN, TransactionStatus.SUCCESS);
 
         // CAPTURE
         final String capture1Key = UUID.randomUUID().toString();
         final Payment partialCapture1 = paymentProcessor.createCapture(true, null, account, paymentId, FIVE, CURRENCY, capture1Key,
                                                                        SHOULD_LOCK_ACCOUNT, PLUGIN_PROPERTIES, callContext, internalCallContext);
-        verifyPayment(partialCapture1, paymentExternalKey, TEN, FIVE, ZERO, 3);
-        verifyPaymentTransaction(partialCapture1.getTransactions().get(2), capture1Key, TransactionType.CAPTURE, FIVE, paymentId);
-        paymentBusListener.verify(3, account.getId(), paymentId, FIVE);
+        verifyPayment(partialCapture1, paymentExternalKey, TEN, FIVE, ZERO, 2);
+        verifyPaymentTransaction(partialCapture1.getTransactions().get(1), capture1Key, TransactionType.CAPTURE, FIVE, paymentId);
+        paymentBusListener.verify(3, account.getId(), paymentId, FIVE, TransactionStatus.SUCCESS);
 
         // CAPTURE
         final String capture2Key = UUID.randomUUID().toString();
         final Payment partialCapture2 = paymentProcessor.createCapture(true, null, account, paymentId, FIVE, CURRENCY, capture2Key,
                                                                        SHOULD_LOCK_ACCOUNT, PLUGIN_PROPERTIES, callContext, internalCallContext);
-        verifyPayment(partialCapture2, paymentExternalKey, TEN, TEN, ZERO, 4);
-        verifyPaymentTransaction(partialCapture2.getTransactions().get(3), capture2Key, TransactionType.CAPTURE, FIVE, paymentId);
-        paymentBusListener.verify(4, account.getId(), paymentId, FIVE);
+        verifyPayment(partialCapture2, paymentExternalKey, TEN, TEN, ZERO, 3);
+        verifyPaymentTransaction(partialCapture2.getTransactions().get(2), capture2Key, TransactionType.CAPTURE, FIVE, paymentId);
+        paymentBusListener.verify(4, account.getId(), paymentId, FIVE, TransactionStatus.SUCCESS);
 
         // REFUND
         final String refund1Key = UUID.randomUUID().toString();
         final Payment partialRefund1 = paymentProcessor.createRefund(true, null, account, paymentId, FIVE, CURRENCY, refund1Key,
                                                                      SHOULD_LOCK_ACCOUNT, PLUGIN_PROPERTIES, callContext, internalCallContext);
-        verifyPayment(partialRefund1, paymentExternalKey, TEN, TEN, FIVE, 5);
-        verifyPaymentTransaction(partialRefund1.getTransactions().get(4), refund1Key, TransactionType.REFUND, FIVE, paymentId);
-        paymentBusListener.verify(5, account.getId(), paymentId, FIVE);
+        verifyPayment(partialRefund1, paymentExternalKey, TEN, TEN, FIVE, 4);
+        verifyPaymentTransaction(partialRefund1.getTransactions().get(3), refund1Key, TransactionType.REFUND, FIVE, paymentId);
+        paymentBusListener.verify(5, account.getId(), paymentId, FIVE, TransactionStatus.SUCCESS);
 
         // REFUND
         final String refund2Key = UUID.randomUUID().toString();
         final Payment partialRefund2 = paymentProcessor.createRefund(true, null, account, paymentId, FIVE, CURRENCY, refund2Key,
                                                                      SHOULD_LOCK_ACCOUNT, PLUGIN_PROPERTIES, callContext, internalCallContext);
-        verifyPayment(partialRefund2, paymentExternalKey, TEN, TEN, TEN, 6);
-        verifyPaymentTransaction(partialRefund2.getTransactions().get(5), refund2Key, TransactionType.REFUND, FIVE, paymentId);
-        paymentBusListener.verify(6, account.getId(), paymentId, FIVE);
+        verifyPayment(partialRefund2, paymentExternalKey, TEN, TEN, TEN, 5);
+        verifyPaymentTransaction(partialRefund2.getTransactions().get(4), refund2Key, TransactionType.REFUND, FIVE, paymentId);
+        paymentBusListener.verify(6, account.getId(), paymentId, FIVE, TransactionStatus.SUCCESS);
     }
 
     @Test(groups = "slow")
@@ -132,7 +136,7 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
         verifyPayment(authorization, paymentExternalKey, TEN, ZERO, ZERO, 1);
         final UUID paymentId = authorization.getId();
         verifyPaymentTransaction(authorization.getTransactions().get(0), authorizationKey, TransactionType.AUTHORIZE, TEN, paymentId);
-        paymentBusListener.verify(1, account.getId(), paymentId, TEN);
+        paymentBusListener.verify(1, account.getId(), paymentId, TEN, TransactionStatus.SUCCESS);
 
         // VOID
         final String voidKey = UUID.randomUUID().toString();
@@ -140,7 +144,7 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
                                                                     SHOULD_LOCK_ACCOUNT, PLUGIN_PROPERTIES, callContext, internalCallContext);
         verifyPayment(voidTransaction, paymentExternalKey, ZERO, ZERO, ZERO, 2);
         verifyPaymentTransaction(voidTransaction.getTransactions().get(1), voidKey, TransactionType.VOID, null, paymentId);
-        paymentBusListener.verify(2, account.getId(), paymentId, null);
+        paymentBusListener.verify(2, account.getId(), paymentId, null, TransactionStatus.SUCCESS);
     }
 
     @Test(groups = "slow")
@@ -154,7 +158,7 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
         verifyPayment(purchase, paymentExternalKey, ZERO, ZERO, ZERO, 1);
         final UUID paymentId = purchase.getId();
         verifyPaymentTransaction(purchase.getTransactions().get(0), purchaseKey, TransactionType.PURCHASE, TEN, paymentId);
-        paymentBusListener.verify(1, account.getId(), paymentId, TEN);
+        paymentBusListener.verify(1, account.getId(), paymentId, TEN, TransactionStatus.SUCCESS);
     }
 
     @Test(groups = "slow")
@@ -168,7 +172,7 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
         verifyPayment(purchase, paymentExternalKey, ZERO, ZERO, ZERO, 1);
         final UUID paymentId = purchase.getId();
         verifyPaymentTransaction(purchase.getTransactions().get(0), creditKey, TransactionType.CREDIT, TEN, paymentId);
-        paymentBusListener.verify(1, account.getId(), paymentId, TEN);
+        paymentBusListener.verify(1, account.getId(), paymentId, TEN, TransactionStatus.SUCCESS);
     }
 
     private void verifyPayment(final Payment payment, final String paymentExternalKey,
@@ -223,7 +227,7 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
             paymentPluginErrorEvents.add(event);
         }
 
-        public void verify(final int eventNb, final UUID accountId, final UUID paymentId, final BigDecimal amount) throws Exception {
+        private void verify(final int eventNb, final UUID accountId, final UUID paymentId, final BigDecimal amount, final TransactionStatus transactionStatus) throws Exception {
             Awaitility.await()
                       .until(new Callable<Boolean>() {
                           @Override
@@ -234,10 +238,10 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
             Assert.assertEquals(paymentErrorEvents.size(), 0);
             Assert.assertEquals(paymentPluginErrorEvents.size(), 0);
 
-            verify(paymentInfoEvents.get(eventNb - 1), accountId, paymentId, amount);
+            verify(paymentInfoEvents.get(eventNb - 1), accountId, paymentId, amount, transactionStatus);
         }
 
-        private void verify(final PaymentInfoInternalEvent event, final UUID accountId, final UUID paymentId, @Nullable final BigDecimal amount) {
+        private void verify(final PaymentInfoInternalEvent event, final UUID accountId, final UUID paymentId, @Nullable final BigDecimal amount, final TransactionStatus transactionStatus) {
             Assert.assertEquals(event.getPaymentId(), paymentId);
             Assert.assertEquals(event.getAccountId(), accountId);
             if (amount == null) {
@@ -245,7 +249,7 @@ public class TestPaymentProcessor extends PaymentTestSuiteWithEmbeddedDB {
             } else {
                 Assert.assertEquals(event.getAmount().compareTo(amount), 0);
             }
-            Assert.assertEquals(event.getStatus(), TransactionStatus.SUCCESS);
+            Assert.assertEquals(event.getStatus(), transactionStatus);
         }
     }
 }
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
index d479336..3f314c8 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
@@ -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
@@ -50,6 +50,7 @@ public class MockPaymentDao implements PaymentDao {
     private final Map<UUID, PaymentAttemptModelDao> attempts = new HashMap<UUID, PaymentAttemptModelDao>();
 
     private final MockNonEntityDao mockNonEntityDao;
+    private final List<PaymentMethodModelDao> paymentMethods = new LinkedList<PaymentMethodModelDao>();
 
     @Inject
     public MockPaymentDao(final MockNonEntityDao mockNonEntityDao) {
@@ -59,6 +60,7 @@ public class MockPaymentDao implements PaymentDao {
     public void reset() {
         synchronized (this) {
             payments.clear();
+            paymentMethods.clear();
             transactions.clear();
             attempts.clear();
         }
@@ -66,7 +68,7 @@ public class MockPaymentDao implements PaymentDao {
 
     @Override
     public Pagination<PaymentTransactionModelDao> getByTransactionStatusAcrossTenants(final Iterable<TransactionStatus> transactionStatuses, DateTime createdBeforeDate, DateTime createdAfterDate, Long offset, Long limit) {
-        final List<PaymentTransactionModelDao> result=  ImmutableList.copyOf(Iterables.filter(transactions.values(), new Predicate<PaymentTransactionModelDao>() {
+        final List<PaymentTransactionModelDao> result = ImmutableList.copyOf(Iterables.filter(transactions.values(), new Predicate<PaymentTransactionModelDao>() {
             @Override
             public boolean apply(final PaymentTransactionModelDao input) {
                 return Iterables.any(transactionStatuses, new Predicate<TransactionStatus>() {
@@ -311,11 +313,11 @@ public class MockPaymentDao implements PaymentDao {
         }
     }
 
-    private final List<PaymentMethodModelDao> paymentMethods = new LinkedList<PaymentMethodModelDao>();
-
     @Override
     public PaymentMethodModelDao insertPaymentMethod(final PaymentMethodModelDao paymentMethod, final InternalCallContext context) {
         synchronized (this) {
+            paymentMethod.setAccountRecordId(context.getAccountRecordId());
+            paymentMethod.setTenantRecordId(context.getTenantRecordId());
             paymentMethods.add(paymentMethod);
             return paymentMethod;
         }
@@ -346,11 +348,11 @@ public class MockPaymentDao implements PaymentDao {
     }
 
     @Override
-    public List<PaymentMethodModelDao> getPaymentMethods(final UUID accountId, final InternalTenantContext context) {
+    public List<PaymentMethodModelDao> getPaymentMethods(final InternalTenantContext context) {
         synchronized (this) {
             final List<PaymentMethodModelDao> result = new ArrayList<PaymentMethodModelDao>();
             for (final PaymentMethodModelDao cur : paymentMethods) {
-                if (cur.getAccountId().equals(accountId)) {
+                if (cur.getAccountRecordId().equals(context.getAccountRecordId())) {
                     result.add(cur);
                 }
             }
@@ -383,7 +385,7 @@ public class MockPaymentDao implements PaymentDao {
     }
 
     @Override
-    public List<PaymentMethodModelDao> refreshPaymentMethods(final UUID accountId, final String pluginName, final List<PaymentMethodModelDao> paymentMethods, final InternalCallContext context) {
+    public List<PaymentMethodModelDao> refreshPaymentMethods(final String pluginName, final List<PaymentMethodModelDao> paymentMethods, final InternalCallContext context) {
         return ImmutableList.<PaymentMethodModelDao>of();
     }
 
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
index d75b656..878ee98 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
@@ -232,7 +232,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
         assertEquals(savedMethod.getPluginName(), pluginName);
         assertEquals(savedMethod.isActive(), isActive);
 
-        final List<PaymentMethodModelDao> result = paymentDao.getPaymentMethods(accountId, internalCallContext);
+        final List<PaymentMethodModelDao> result = paymentDao.getPaymentMethods(internalCallContext);
         assertEquals(result.size(), 1);
         savedMethod = result.get(0);
         assertEquals(savedMethod.getId(), paymentMethodId);
diff --git a/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java b/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java
index f2cd9c8..b582f33 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java
@@ -30,6 +30,7 @@ import org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcher
 import org.killbill.billing.util.UUIDs;
 import org.killbill.commons.request.Request;
 import org.killbill.commons.request.RequestData;
+import org.slf4j.MDC;
 import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -140,6 +141,7 @@ public class TestPluginDispatcher extends PaymentTestSuiteNoDB {
                                                                                                                                                      UUIDs.getRandom(),
                                                                                                                                                      null,
                                                                                                                                                      null,
+                                                                                                                                                     MDC.getCopyOfContextMap(),
                                                                                                                                                      delegate);
 
         final String actualRequestId = stringPluginDispatcher.dispatchWithTimeout(callable, 100, TimeUnit.MILLISECONDS);
diff --git a/payment/src/test/java/org/killbill/billing/payment/logging/SpyLogger.java b/payment/src/test/java/org/killbill/billing/payment/logging/SpyLogger.java
index 62efbe4..56270cd 100644
--- a/payment/src/test/java/org/killbill/billing/payment/logging/SpyLogger.java
+++ b/payment/src/test/java/org/killbill/billing/payment/logging/SpyLogger.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -14,12 +14,15 @@
  * License for the specific language governing permissions and limitations
  * under the License.
  */
+
 package org.killbill.billing.payment.logging;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
 import java.util.regex.Pattern;
 
+import org.slf4j.event.SubstituteLoggingEvent;
 import org.slf4j.helpers.FormattingTuple;
 import org.slf4j.helpers.MessageFormatter;
 import org.slf4j.helpers.SubstituteLogger;
@@ -28,10 +31,10 @@ import com.google.common.base.Optional;
 
 public class SpyLogger extends SubstituteLogger {
 
-    private List<LogMessage> logMessageList = new ArrayList<LogMessage>();
+    private final List<LogMessage> logMessageList = new ArrayList<LogMessage>();
 
     public SpyLogger(String loggerName) {
-        super(loggerName);
+        super(loggerName, new LinkedBlockingQueue<SubstituteLoggingEvent>(), false);
     }
 
     public static final String LOG_LEVEL_TRACE = "TRACE";
diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
index 9af2ee5..16ef9fe 100644
--- a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
@@ -30,6 +30,7 @@ import org.killbill.billing.payment.core.PaymentProcessor;
 import org.killbill.billing.payment.core.PluginControlPaymentProcessor;
 import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
 import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner;
+import org.killbill.billing.payment.dao.MockPaymentDao;
 import org.killbill.billing.payment.dao.PaymentDao;
 import org.killbill.billing.payment.glue.TestPaymentModuleNoDB;
 import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
@@ -104,6 +105,7 @@ public abstract class PaymentTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
     public void beforeMethod() throws Exception {
         eventBus.start();
         paymentExecutors.initialize();
+        ((MockPaymentDao) paymentDao).reset();
         Profiling.resetPerThreadProfilingData();
     }
 
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
index b5462f5..6949690 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
@@ -34,7 +34,6 @@ import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.payment.api.Payment;
 import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.api.PluginProperty;
-import org.killbill.billing.payment.dao.MockPaymentDao;
 import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
 import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
 import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
@@ -70,7 +69,6 @@ public class TestRetryService extends PaymentTestSuiteNoDB {
         setDefaultPollInterval(Duration.ONE_HUNDRED_MILLISECONDS);
         Awaitility.setDefaultPollDelay(Duration.SAME_AS_POLL_INTERVAL);
 
-        ((MockPaymentDao) paymentDao).reset();
         mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(MockPaymentProviderPlugin.PLUGIN_NAME);
         mockPaymentProviderPlugin.clear();
         retryService.initialize();

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 1c0e5ff..2056c73 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill-oss-parent</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.86</version>
+        <version>0.90</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.16.4-SNAPSHOT</version>
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/DefaultServerService.java b/profiles/killbill/src/main/java/org/killbill/billing/server/DefaultServerService.java
index 0306248..a085bfa 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/DefaultServerService.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/DefaultServerService.java
@@ -55,7 +55,7 @@ public class DefaultServerService implements ServerService {
         try {
             bus.register(pushNotificationListener);
         } catch (final EventBusException e) {
-            log.warn("Failed to initialize Server service :", e);
+            log.warn("Failed to register PushNotificationListener", e);
         }
     }
 
@@ -64,7 +64,7 @@ public class DefaultServerService implements ServerService {
         try {
             bus.unregister(pushNotificationListener);
         } catch (final EventBusException e) {
-            log.warn("Failed to stop Server service :", e);
+            log.warn("Failed to unregister PushNotificationListener", e);
         }
     }
 }
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/filters/KillbillMDCInsertingServletFilter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/filters/KillbillMDCInsertingServletFilter.java
new file mode 100644
index 0000000..7782a51
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/filters/KillbillMDCInsertingServletFilter.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.filters;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.killbill.commons.request.Request;
+import org.killbill.commons.request.RequestData;
+import org.slf4j.MDC;
+
+import com.google.inject.Singleton;
+import com.sun.jersey.spi.container.ContainerRequest;
+import com.sun.jersey.spi.container.ContainerRequestFilter;
+import com.sun.jersey.spi.container.ContainerResponse;
+import com.sun.jersey.spi.container.ContainerResponseFilter;
+import com.sun.jersey.spi.container.ContainerResponseWriter;
+
+import static org.killbill.billing.util.callcontext.InternalCallContextFactory.MDC_KB_ACCOUNT_RECORD_ID;
+import static org.killbill.billing.util.callcontext.InternalCallContextFactory.MDC_KB_TENANT_RECORD_ID;
+
+@Singleton
+public class KillbillMDCInsertingServletFilter implements ContainerRequestFilter, ContainerResponseFilter {
+
+    private static final String MDC_REQUEST_ID = "req.requestId";
+
+    @Override
+    public ContainerRequest filter(final ContainerRequest request) {
+        final RequestData perThreadRequestData = Request.getPerThreadRequestData();
+        if (perThreadRequestData != null) {
+            MDC.put(MDC_REQUEST_ID, perThreadRequestData.getRequestId());
+        }
+
+        return request;
+    }
+
+    @Override
+    public ContainerResponse filter(final ContainerRequest request, final ContainerResponse response) {
+        response.setContainerResponseWriter(new Adapter(response.getContainerResponseWriter()));
+        return response;
+    }
+
+    private static final class Adapter implements ContainerResponseWriter {
+
+        private final ContainerResponseWriter crw;
+
+        Adapter(final ContainerResponseWriter containerResponseWriter) {
+            this.crw = containerResponseWriter;
+        }
+
+        @Override
+        public OutputStream writeStatusAndHeaders(final long contentLength, final ContainerResponse response) throws IOException {
+            return crw.writeStatusAndHeaders(contentLength, response);
+        }
+
+        @Override
+        public void finish() throws IOException {
+            crw.finish();
+
+            // Removing possibly inexistent item is OK
+            MDC.remove(MDC_REQUEST_ID);
+
+            // Cleanup
+            MDC.remove(MDC_KB_ACCOUNT_RECORD_ID);
+            MDC.remove(MDC_KB_TENANT_RECORD_ID);
+        }
+    }
+}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/filters/ProfilingContainerResponseFilter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/filters/ProfilingContainerResponseFilter.java
index 38402b5..9f7e05f 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/filters/ProfilingContainerResponseFilter.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/filters/ProfilingContainerResponseFilter.java
@@ -63,7 +63,7 @@ public class ProfilingContainerResponseFilter implements ContainerRequestFilter,
                     profilingData.addStart(ProfilingFeatureType.JAXRS, request.getPath());
                 }
             } catch (IllegalArgumentException e) {
-                log.info("Profiling data output " + profilingHeaderRequest + " is not supported, profiling NOT enabled");
+                log.info("Profiling data output {} is not supported, profiling NOT enabled", profilingHeaderRequest);
             }
         }
         return request;
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/filters/RequestDataFilter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/filters/RequestDataFilter.java
index 02fa786..33a7879 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/filters/RequestDataFilter.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/filters/RequestDataFilter.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -17,6 +17,8 @@
 
 package org.killbill.billing.server.filters;
 
+import java.io.IOException;
+import java.io.OutputStream;
 import java.util.List;
 
 import javax.ws.rs.core.HttpHeaders;
@@ -30,6 +32,7 @@ import com.sun.jersey.spi.container.ContainerRequest;
 import com.sun.jersey.spi.container.ContainerRequestFilter;
 import com.sun.jersey.spi.container.ContainerResponse;
 import com.sun.jersey.spi.container.ContainerResponseFilter;
+import com.sun.jersey.spi.container.ContainerResponseWriter;
 
 @Singleton
 public class RequestDataFilter implements ContainerRequestFilter, ContainerResponseFilter {
@@ -48,7 +51,7 @@ public class RequestDataFilter implements ContainerRequestFilter, ContainerRespo
 
     @Override
     public ContainerResponse filter(final ContainerRequest request, final ContainerResponse response) {
-        Request.resetPerThreadRequestData();
+        response.setContainerResponseWriter(new Adapter(response.getContainerResponseWriter()));
         return response;
     }
 
@@ -59,4 +62,26 @@ public class RequestDataFilter implements ContainerRequestFilter, ContainerRespo
         }
         return requestIds;
     }
+
+    private static final class Adapter implements ContainerResponseWriter {
+
+        private final ContainerResponseWriter crw;
+
+        Adapter(final ContainerResponseWriter containerResponseWriter) {
+            this.crw = containerResponseWriter;
+        }
+
+        @Override
+        public OutputStream writeStatusAndHeaders(final long contentLength, final ContainerResponse response) throws IOException {
+            return crw.writeStatusAndHeaders(contentLength, response);
+        }
+
+        @Override
+        public void finish() throws IOException {
+            crw.finish();
+
+            // Reset the per-thread RequestData last
+            Request.resetPerThreadRequestData();
+        }
+    }
 }
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/listeners/KillbillGuiceListener.java b/profiles/killbill/src/main/java/org/killbill/billing/server/listeners/KillbillGuiceListener.java
index e031a5d..51a2a75 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/listeners/KillbillGuiceListener.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/listeners/KillbillGuiceListener.java
@@ -27,6 +27,7 @@ import org.killbill.billing.jaxrs.resources.JaxRsResourceBase;
 import org.killbill.billing.jaxrs.util.KillbillEventHandler;
 import org.killbill.billing.platform.api.KillbillConfigSource;
 import org.killbill.billing.platform.config.DefaultKillbillConfigSource;
+import org.killbill.billing.server.filters.KillbillMDCInsertingServletFilter;
 import org.killbill.billing.server.filters.ProfilingContainerResponseFilter;
 import org.killbill.billing.server.filters.RequestDataFilter;
 import org.killbill.billing.server.filters.ResponseCorsFilter;
@@ -34,11 +35,10 @@ import org.killbill.billing.server.modules.KillbillServerModule;
 import org.killbill.billing.server.security.TenantFilter;
 import org.killbill.bus.api.PersistentBus;
 import org.killbill.commons.skeleton.modules.BaseServerModuleBuilder;
-import org.slf4j.ILoggerFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.helpers.MDCInsertingServletFilter;
 import com.google.common.collect.ImmutableMap;
 import com.google.inject.Module;
 import com.google.inject.servlet.ServletModule;
@@ -63,10 +63,14 @@ public class KillbillGuiceListener extends KillbillPlatformGuiceListener {
                                                                              // Swagger integration
                                                                              .addJaxrsResource("com.wordnik.swagger.jersey.listing");
 
-        //
-        // Add jersey filters which are executed prior jersey write the output stream
-        //
-        builder.addJerseyFilter("com.sun.jersey.api.container.filter.LoggingFilter");
+        // Set the per-thread RequestData first
+        builder.addJerseyFilter(RequestDataFilter.class.getName());
+
+        // Logback default MDC
+        builder.addFilter("/*", MDCInsertingServletFilter.class);
+
+        // Kill Bill specific MDC
+        builder.addJerseyFilter(KillbillMDCInsertingServletFilter.class.getName());
 
         // Disable WADL - it generates noisy log messages, such as:
         // c.s.j.s.w.g.AbstractWadlGeneratorGrammarGenerator - Couldn't find grammar element for class javax.ws.rs.core.Response
@@ -79,16 +83,19 @@ public class KillbillGuiceListener extends KillbillPlatformGuiceListener {
             builder.addJerseyFilter(GZIPContentEncodingFilter.class.getName());
         }
         builder.addJerseyFilter(ProfilingContainerResponseFilter.class.getName());
-        builder.addJerseyFilter(RequestDataFilter.class.getName());
 
         // Broader, to support the "Try it out!" feature
         //builder.addFilter("/" + SWAGGER_PATH + "*", ResponseCorsFilter.class);
         builder.addFilter("/*", ResponseCorsFilter.class);
 
-        // Add TenantFilter right after is multi-tenancy has been configured.
+        // Add TenantFilter right after if multi-tenancy has been configured.
         if (config.isMultiTenancyEnabled()) {
             builder.addFilter("/*", TenantFilter.class);
         }
+
+        // Finally, just before the request starts, enable the LoggingFilter
+        builder.addJerseyFilter("com.sun.jersey.api.container.filter.LoggingFilter");
+
         return builder.build();
     }
 
@@ -105,18 +112,6 @@ public class KillbillGuiceListener extends KillbillPlatformGuiceListener {
     }
 
     @Override
-    protected void startLifecycleStage1() {
-        super.startLifecycleStage1();
-
-        // Work-around for http://jira.qos.ch/browse/LOGBACK-730
-        final ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory();
-        if (iLoggerFactory instanceof LoggerContext) {
-            final LoggerContext lc = (LoggerContext) iLoggerFactory;
-            lc.setPackagingDataEnabled(false);
-        }
-    }
-
-    @Override
     protected void startLifecycleStage2() {
         killbilleventHandler = injector.getInstance(KillbillEventHandler.class);
 
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java
index 9b2a102..c8c94c2 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java
@@ -90,6 +90,7 @@ import org.killbill.commons.jdbi.mapper.LowerToCamelBeanMapperFactory;
 import org.skife.jdbi.v2.ResultSetMapperFactory;
 import org.skife.jdbi.v2.tweak.ResultSetMapper;
 
+import ch.qos.logback.classic.helpers.MDCInsertingServletFilter;
 import com.google.inject.multibindings.Multibinder;
 
 public class KillbillServerModule extends KillbillPlatformModule {
@@ -200,6 +201,7 @@ public class KillbillServerModule extends KillbillPlatformModule {
 
     protected void configureFilters() {
         bind(ResponseCorsFilter.class).asEagerSingleton();
+        bind(MDCInsertingServletFilter.class).asEagerSingleton();
     }
 
     protected void configurePushNotification() {
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/notifications/PushNotificationListener.java b/profiles/killbill/src/main/java/org/killbill/billing/server/notifications/PushNotificationListener.java
index d821177..7e3a993 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/notifications/PushNotificationListener.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/notifications/PushNotificationListener.java
@@ -113,7 +113,7 @@ public class PushNotificationListener {
                     });
             response = futureStatus.get(timeoutSec, TimeUnit.SECONDS);
         } catch (final Exception e) {
-            log.warn(String.format("Failed to push notification %s for the tenant %s", url, tenantId), e);
+            log.warn("Failed to push notification url='{}', tenantId='{}'", url, tenantId, e);
             return false;
         }
         return response.getStatusCode() >= 200 && response.getStatusCode() < 300;
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
index 7f2a2bb..8ce5b3e 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
@@ -37,6 +37,7 @@ import org.apache.shiro.authc.UsernamePasswordToken;
 import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
 import org.apache.shiro.realm.Realm;
 import org.killbill.billing.jaxrs.resources.JaxrsResource;
+import org.killbill.billing.jaxrs.util.Context;
 import org.killbill.billing.server.listeners.KillbillGuiceListener;
 import org.killbill.billing.tenant.api.Tenant;
 import org.killbill.billing.tenant.api.TenantApiException;
@@ -55,6 +56,8 @@ public class TenantFilter implements Filter {
     private static final Logger log = LoggerFactory.getLogger(TenantFilter.class);
 
     @Inject
+    protected Context context;
+    @Inject
     protected TenantUserApi tenantUserApi;
     @Inject
     protected KillbillJdbcTenantRealm killbillJdbcTenantRealm;
@@ -101,10 +104,13 @@ public class TenantFilter implements Filter {
             final Tenant tenant = tenantUserApi.getTenantByApiKey(apiKey);
             request.setAttribute(TENANT, tenant);
 
+            // Create a dummy context, to set the MDC very early for LoggingFilter
+            context.createContext(request);
+
             chain.doFilter(request, response);
         } catch (final TenantApiException e) {
             // Should never happen since Shiro validated the credentials?
-            log.warn("Couldn't find the tenant?", e);
+            log.error("Couldn't find the tenant? - should never happen!", e);
         }
     }
 
diff --git a/profiles/killbill/src/main/resources/logback.xml b/profiles/killbill/src/main/resources/logback.xml
index 3e86876..f2cdef8 100644
--- a/profiles/killbill/src/main/resources/logback.xml
+++ b/profiles/killbill/src/main/resources/logback.xml
@@ -21,7 +21,8 @@
 
     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>
-            <pattern>%date [%thread] %-5level %logger{36} - %maskedMsg%n%ex</pattern>
+            <!-- See http://jira.qos.ch/browse/LOGBACK-262 -->
+            <pattern>%date{"yyyy-MM-dd'T'HH:mm:ss,SSSZ", UTC} lvl='%level', log='%logger{0}', th='%thread', xff='%X{req.xForwardedFor}', rId='%X{req.requestId}', aRId='%X{kb.accountRecordId}', tRId='%X{kb.tenantRecordId}', %maskedMsg%n</pattern>
         </encoder>
     </appender>
 
@@ -42,7 +43,7 @@
                     </timeBasedFileNamingAndTriggeringPolicy>
                 </rollingPolicy>
                 <encoder>
-                    <pattern>%date [%thread] %maskedMsg%n%ex</pattern>
+                    <pattern>%date [%thread] %maskedMsg%n</pattern>
                 </encoder>
             </appender>
         </sift>
@@ -63,7 +64,7 @@
                     </timeBasedFileNamingAndTriggeringPolicy>
                 </rollingPolicy>
                 <encoder>
-                    <pattern>%date [%thread] %maskedMsg%n%ex</pattern>
+                    <pattern>%date [%thread] %maskedMsg%n</pattern>
                 </encoder>
             </appender>
         </sift>
@@ -84,7 +85,7 @@
                     </timeBasedFileNamingAndTriggeringPolicy>
                 </rollingPolicy>
                 <encoder>
-                    <pattern>%date [%thread] %maskedMsg%n%ex</pattern>
+                    <pattern>%date [%thread] %maskedMsg%n</pattern>
                 </encoder>
             </appender>
         </sift>
@@ -105,7 +106,7 @@
                     </timeBasedFileNamingAndTriggeringPolicy>
                 </rollingPolicy>
                 <encoder>
-                    <pattern>%date [%thread] %maskedMsg%n%ex</pattern>
+                    <pattern>%date [%thread] %maskedMsg%n</pattern>
                 </encoder>
             </appender>
         </sift>
@@ -126,7 +127,7 @@
                     </timeBasedFileNamingAndTriggeringPolicy>
                 </rollingPolicy>
                 <encoder>
-                    <pattern>%date [%thread] %maskedMsg%n%ex</pattern>
+                    <pattern>%date [%thread] %maskedMsg%n</pattern>
                 </encoder>
             </appender>
         </sift>
@@ -147,7 +148,7 @@
                     </timeBasedFileNamingAndTriggeringPolicy>
                 </rollingPolicy>
                 <encoder>
-                    <pattern>%date [%thread] %maskedMsg%n%ex</pattern>
+                    <pattern>%date [%thread] %maskedMsg%n</pattern>
                 </encoder>
             </appender>
         </sift>
@@ -178,6 +179,9 @@
         <appender-ref ref="SIFT-jdbc-connection"/>
     </logger>
 
+    <!-- See https://github.com/jOOQ/jOOQ/issues/4019 -->
+    <logger name="org.jooq.Constants" level="OFF"/>
+
     <!-- Silence verbose loggers in DEBUG mode -->
     <logger name="com.dmurph" level="OFF"/>
     <logger name="org.killbill.billing.notificationq" level="INFO"/>
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestChargeback.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestChargeback.java
index 50e2a2d..a8eb3bd 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestChargeback.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestChargeback.java
@@ -123,7 +123,6 @@ public class TestChargeback extends TestJaxrsBase {
 
         final InvoicePaymentTransaction input = new InvoicePaymentTransaction();
         input.setPaymentId(payment.getPaymentId());
-        input.setAmount(BigDecimal.TEN.negate());
 
         try {
             killBillClient.createInvoicePaymentChargeback(input, createdBy, reason, comment);
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
index f193612..7606915 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
@@ -164,7 +164,7 @@ public class TestInvoice extends TestJaxrsBase {
                                   "                <td>shotgun-monthly-trial</td>\n" +
                                   "                <td>Monthly shotgun plan</td>\n" +
                                   "                <td>25 avr. 2012</td>\n" +
-                                  "                <td>USD 0E-9</td>\n" +
+                                  "                <td>USD 0.00</td>\n" +
                                   "            </tr>\n" +
                                   "            \n" +
                                   "            <tr>\n" +
@@ -178,7 +178,7 @@ public class TestInvoice extends TestJaxrsBase {
                                   "            <tr>\n" +
                                   "                <td colspan=2 />\n" +
                                   "                <td align=right><strong>invoiceAmountPaid</strong></td>\n" +
-                                  "                <td align=right><strong>0</strong></td>\n" +
+                                  "                <td align=right><strong>0.00</strong></td>\n" +
                                   "            </tr>\n" +
                                   "            <tr>\n" +
                                   "                <td colspan=2 />\n" +
@@ -410,7 +410,7 @@ public class TestInvoice extends TestJaxrsBase {
         assertEquals(invoiceItem.getAmount().compareTo(BigDecimal.ZERO), 1);
 
         // Adjust partially the item
-        final BigDecimal adjustedAmount = invoiceItem.getAmount().divide(BigDecimal.TEN);
+        final BigDecimal adjustedAmount = invoiceItem.getAmount().divide(BigDecimal.TEN, BigDecimal.ROUND_HALF_UP);
         final InvoiceItem adjustmentInvoiceItem = new InvoiceItem();
         adjustmentInvoiceItem.setAccountId(accountJson.getAccountId());
         adjustmentInvoiceItem.setInvoiceId(invoice.getInvoiceId());
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
index b76434b..b46915f 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
@@ -33,6 +33,7 @@ import org.killbill.billing.client.model.PaymentTransaction;
 import org.killbill.billing.client.model.Payments;
 import org.killbill.billing.client.model.PluginProperty;
 import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.api.TransactionStatus;
 import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
 import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
@@ -47,6 +48,7 @@ import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
 
 public class TestPayment extends TestJaxrsBase {
 
@@ -139,6 +141,185 @@ public class TestPayment extends TestJaxrsBase {
     }
 
     @Test(groups = "slow")
+    public void testAuthorizeCompletionUsingPaymentId() throws Exception {
+        final Account account = createAccountWithDefaultPaymentMethod();
+        final UUID paymentMethodId = account.getPaymentMethodId();
+        final BigDecimal amount = BigDecimal.TEN;
+
+        final String pending = PaymentPluginStatus.PENDING.toString();
+        final ImmutableMap<String, String> pendingPluginProperties = ImmutableMap.<String, String>of(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, pending);
+
+        final ImmutableMap<String, String> pluginProperties = ImmutableMap.of();
+
+        TransactionType transactionType = TransactionType.AUTHORIZE;
+        final String paymentExternalKey = UUID.randomUUID().toString();
+        final String authTransactionExternalKey = UUID.randomUUID().toString();
+
+        final Payment initialPayment = createVerifyTransaction(account, paymentMethodId, paymentExternalKey, authTransactionExternalKey, transactionType, pending, amount, BigDecimal.ZERO, pendingPluginProperties, 1);
+
+        // Complete operation: first, only specify the payment id
+        final PaymentTransaction completeTransactionByPaymentId = new PaymentTransaction();
+        completeTransactionByPaymentId.setPaymentId(initialPayment.getPaymentId());
+        final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentId, pluginProperties, createdBy, reason, comment);
+        verifyPayment(account, paymentMethodId, completedPaymentByPaymentId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), TransactionStatus.SUCCESS.name(), amount, amount, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
+    }
+
+
+    @Test(groups = "slow")
+    public void testAuthorizeCompletionUsingPaymentIdAndTransactionId() throws Exception {
+        final Account account = createAccountWithDefaultPaymentMethod();
+        final UUID paymentMethodId = account.getPaymentMethodId();
+        final BigDecimal amount = BigDecimal.TEN;
+
+        final String pending = PaymentPluginStatus.PENDING.toString();
+        final ImmutableMap<String, String> pendingPluginProperties = ImmutableMap.<String, String>of(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, pending);
+
+        final ImmutableMap<String, String> pluginProperties = ImmutableMap.of();
+
+        TransactionType transactionType = TransactionType.AUTHORIZE;
+        final String paymentExternalKey = UUID.randomUUID().toString();
+        final String authTransactionExternalKey = UUID.randomUUID().toString();
+
+        final Payment initialPayment = createVerifyTransaction(account, paymentMethodId, paymentExternalKey, authTransactionExternalKey, transactionType, pending, amount, BigDecimal.ZERO, pendingPluginProperties, 1);
+
+
+        final PaymentTransaction completeTransactionByPaymentIdAndInvalidTransactionId = new PaymentTransaction();
+        completeTransactionByPaymentIdAndInvalidTransactionId.setPaymentId(initialPayment.getPaymentId());
+        completeTransactionByPaymentIdAndInvalidTransactionId.setTransactionId(UUID.randomUUID());
+        try {
+            killBillClient.completePayment(completeTransactionByPaymentIdAndInvalidTransactionId, pluginProperties, createdBy, reason, comment);
+            fail("Payment completion should fail when invalid transaction id has been provided" );
+        } catch (final KillBillClientException expected) {
+        }
+
+        final PaymentTransaction completeTransactionByPaymentIdAndTransactionId = new PaymentTransaction();
+        completeTransactionByPaymentIdAndTransactionId.setPaymentId(initialPayment.getPaymentId());
+        completeTransactionByPaymentIdAndTransactionId.setTransactionId(initialPayment.getTransactions().get(0).getTransactionId());
+        final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentIdAndTransactionId, pluginProperties, createdBy, reason, comment);
+        verifyPayment(account, paymentMethodId, completedPaymentByPaymentId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), TransactionStatus.SUCCESS.name(), amount, amount, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
+    }
+
+    @Test(groups = "slow")
+    public void testAuthorizeCompletionUsingPaymentIdAndTransactionExternalKey() throws Exception {
+        final Account account = createAccountWithDefaultPaymentMethod();
+        final UUID paymentMethodId = account.getPaymentMethodId();
+        final BigDecimal amount = BigDecimal.TEN;
+
+        final String pending = PaymentPluginStatus.PENDING.toString();
+        final ImmutableMap<String, String> pendingPluginProperties = ImmutableMap.<String, String>of(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, pending);
+
+        final ImmutableMap<String, String> pluginProperties = ImmutableMap.of();
+
+        TransactionType transactionType = TransactionType.AUTHORIZE;
+        final String paymentExternalKey = UUID.randomUUID().toString();
+        final String authTransactionExternalKey = UUID.randomUUID().toString();
+
+        final Payment initialPayment = createVerifyTransaction(account, paymentMethodId, paymentExternalKey, authTransactionExternalKey, transactionType, pending, amount, BigDecimal.ZERO, pendingPluginProperties, 1);
+
+        final PaymentTransaction completeTransactionByPaymentIdAndInvalidTransactionExternalKey = new PaymentTransaction();
+        completeTransactionByPaymentIdAndInvalidTransactionExternalKey.setPaymentId(initialPayment.getPaymentId());
+        completeTransactionByPaymentIdAndInvalidTransactionExternalKey.setTransactionExternalKey("bozo");
+        try {
+            killBillClient.completePayment(completeTransactionByPaymentIdAndInvalidTransactionExternalKey, pluginProperties, createdBy, reason, comment);
+            fail("Payment completion should fail when invalid transaction externalKey has been provided" );
+        } catch (final KillBillClientException expected) {
+        }
+
+        final PaymentTransaction completeTransactionByPaymentIdAndTransactionExternalKey = new PaymentTransaction();
+        completeTransactionByPaymentIdAndTransactionExternalKey.setPaymentId(initialPayment.getPaymentId());
+        completeTransactionByPaymentIdAndTransactionExternalKey.setTransactionExternalKey(authTransactionExternalKey);
+        final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentIdAndTransactionExternalKey, pluginProperties, createdBy, reason, comment);
+        verifyPayment(account, paymentMethodId, completedPaymentByPaymentId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), TransactionStatus.SUCCESS.name(), amount, amount, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
+    }
+
+
+    @Test(groups = "slow")
+    public void testAuthorizeCompletionUsingPaymentIdAndTransactionType() throws Exception {
+        final Account account = createAccountWithDefaultPaymentMethod();
+        final UUID paymentMethodId = account.getPaymentMethodId();
+        final BigDecimal amount = BigDecimal.TEN;
+
+        final String pending = PaymentPluginStatus.PENDING.toString();
+        final ImmutableMap<String, String> pendingPluginProperties = ImmutableMap.<String, String>of(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, pending);
+
+        final ImmutableMap<String, String> pluginProperties = ImmutableMap.of();
+
+        TransactionType transactionType = TransactionType.AUTHORIZE;
+        final String paymentExternalKey = UUID.randomUUID().toString();
+        final String authTransactionExternalKey = UUID.randomUUID().toString();
+
+        final Payment initialPayment = createVerifyTransaction(account, paymentMethodId, paymentExternalKey, authTransactionExternalKey, transactionType, pending, amount, BigDecimal.ZERO, pendingPluginProperties, 1);
+
+
+        final PaymentTransaction completeTransactionByPaymentIdAndInvalidTransactionType = new PaymentTransaction();
+        completeTransactionByPaymentIdAndInvalidTransactionType.setPaymentId(initialPayment.getPaymentId());
+        completeTransactionByPaymentIdAndInvalidTransactionType.setTransactionType(TransactionType.CAPTURE.name());
+        try {
+            killBillClient.completePayment(completeTransactionByPaymentIdAndInvalidTransactionType, pluginProperties, createdBy, reason, comment);
+            fail("Payment completion should fail when invalid transaction type has been provided" );
+        } catch (final KillBillClientException expected) {
+        }
+
+        final PaymentTransaction completeTransactionByPaymentIdAndTransactionType = new PaymentTransaction();
+        completeTransactionByPaymentIdAndTransactionType.setPaymentId(initialPayment.getPaymentId());
+        completeTransactionByPaymentIdAndTransactionType.setTransactionType(transactionType.name());
+        final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentIdAndTransactionType, pluginProperties, createdBy, reason, comment);
+        verifyPayment(account, paymentMethodId, completedPaymentByPaymentId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), TransactionStatus.SUCCESS.name(), amount, amount, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
+    }
+
+    @Test(groups = "slow")
+    public void testAuthorizeCompletionUsingExternalKey() throws Exception {
+
+        final Account account = createAccountWithDefaultPaymentMethod();
+        final UUID paymentMethodId = account.getPaymentMethodId();
+        final BigDecimal amount = BigDecimal.TEN;
+
+        final String pending = PaymentPluginStatus.PENDING.toString();
+        final ImmutableMap<String, String> pendingPluginProperties = ImmutableMap.<String, String>of(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, pending);
+
+        final ImmutableMap<String, String> pluginProperties = ImmutableMap.of();
+
+        TransactionType transactionType = TransactionType.AUTHORIZE;
+        final String paymentExternalKey = UUID.randomUUID().toString();
+        final String authTransactionExternalKey = UUID.randomUUID().toString();
+
+        final Payment initialPayment = createVerifyTransaction(account, paymentMethodId, paymentExternalKey, authTransactionExternalKey, transactionType, pending, amount, BigDecimal.ZERO, pendingPluginProperties, 1);
+
+        final PaymentTransaction completeTransactionWithTypeAndKey = new PaymentTransaction();
+        completeTransactionWithTypeAndKey.setPaymentId(initialPayment.getPaymentId());
+        completeTransactionWithTypeAndKey.setTransactionExternalKey(authTransactionExternalKey);
+        final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionWithTypeAndKey, pluginProperties, createdBy, reason, comment);
+        verifyPayment(account, paymentMethodId, completedPaymentByPaymentId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), TransactionStatus.SUCCESS.name(), amount, amount, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
+    }
+
+
+    @Test(groups = "slow")
+    public void testAuthorizeInvalidCompletionUsingPaymentId() throws Exception {
+        final Account account = createAccountWithDefaultPaymentMethod();
+        final UUID paymentMethodId = account.getPaymentMethodId();
+        final BigDecimal amount = BigDecimal.TEN;
+
+        final ImmutableMap<String, String> pluginProperties = ImmutableMap.of();
+
+        TransactionType transactionType = TransactionType.AUTHORIZE;
+        final String paymentExternalKey = UUID.randomUUID().toString();
+        final String authTransactionExternalKey = UUID.randomUUID().toString();
+
+        final Payment initialPayment = createVerifyTransaction(account, paymentMethodId, paymentExternalKey, authTransactionExternalKey, transactionType, TransactionStatus.SUCCESS.name(), amount, amount, pluginProperties, 1);
+
+        // The payment was already completed
+        final PaymentTransaction completeTransactionByPaymentId = new PaymentTransaction();
+        completeTransactionByPaymentId.setPaymentId(initialPayment.getPaymentId());
+        try {
+            killBillClient.completePayment(completeTransactionByPaymentId, pluginProperties, createdBy, reason, comment);
+            fail("Completion should not succeed, there is no PENDING payment transaction");
+        } catch (final KillBillClientException expected) {
+            // Invalid parameter paymentId: XXXX
+        }
+    }
+
+
+    @Test(groups = "slow")
     public void testCompletionForSubsequentTransaction() throws Exception {
         final Account account = createAccountWithDefaultPaymentMethod();
         final UUID paymentMethodId = account.getPaymentMethodId();
@@ -163,27 +344,7 @@ public class TestPayment extends TestJaxrsBase {
         final Payment refundPayment = killBillClient.refundPayment(refundTransaction, null, pluginProperties, createdBy, reason, comment);
         verifyPaymentWithPendingRefund(account, paymentMethodId, paymentExternalKey, purchaseTransactionExternalKey, purchaseAmount, refundTransactionExternalKey, refundPayment);
 
-        // We cannot complete using just the payment id as JAX-RS doesn't know which transaction to complete
-        try {
-            final PaymentTransaction completeTransactionByPaymentId = new PaymentTransaction();
-            completeTransactionByPaymentId.setPaymentId(refundPayment.getPaymentId());
-            killBillClient.completePayment(completeTransactionByPaymentId, pluginProperties, createdBy, reason, comment);
-            Assert.fail();
-        } catch (final KillBillClientException e) {
-            assertEquals(e.getMessage(), "PaymentTransactionJson transactionType and externalKey need to be set");
-        }
-
-        // We cannot complete using just the payment external key as JAX-RS doesn't know which transaction to complete
-        try {
-            final PaymentTransaction completeTransactionByPaymentExternalKey = new PaymentTransaction();
-            completeTransactionByPaymentExternalKey.setPaymentExternalKey(refundPayment.getPaymentExternalKey());
-            killBillClient.completePayment(completeTransactionByPaymentExternalKey, pluginProperties, createdBy, reason, comment);
-            Assert.fail();
-        } catch (final KillBillClientException e) {
-            assertEquals(e.getMessage(), "PaymentTransactionJson transactionType and externalKey need to be set");
-        }
 
-        // Finally, it should work if we specify the payment id and transaction external key
         final PaymentTransaction completeTransactionWithTypeAndKey = new PaymentTransaction();
         completeTransactionWithTypeAndKey.setPaymentId(refundPayment.getPaymentId());
         completeTransactionWithTypeAndKey.setTransactionExternalKey(refundTransactionExternalKey);
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index 4b17ee0..278959e 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -315,7 +315,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                     }
                 }
             } catch (final CatalogApiException e) {
-                log.warn("Failed to get subscriptions, ", e);
+                log.warn("Failed to get subscriptions for bundleId='{}'", cur.getId(), e);
                 return null;
             }
         }
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
index 18dd082..e5e84ab 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
@@ -102,7 +102,7 @@ public class DefaultSubscriptionBaseService implements EventListener, Subscripti
                 @Override
                 public void handleReadyNotification(final NotificationEvent inputKey, final DateTime eventDateTime, final UUID fromNotificationQueueUserToken, final Long accountRecordId, final Long tenantRecordId) {
                     if (!(inputKey instanceof SubscriptionNotificationKey)) {
-                        log.error("SubscriptionBase service received an unexpected event type {}" + inputKey.getClass().getName());
+                        log.error("SubscriptionBase service received an unexpected event className='{}'", inputKey.getClass().getName());
                         return;
                     }
 
@@ -110,7 +110,7 @@ public class DefaultSubscriptionBaseService implements EventListener, Subscripti
                     final SubscriptionBaseEvent event = dao.getEventById(key.getEventId(), internalCallContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId));
                     if (event == null) {
                         // This can be expected if the event is soft deleted (is_active = 0)
-                        log.info("Failed to extract event for notification key {}", inputKey);
+                        log.debug("Failed to extract event for notification key {}", inputKey);
                         return;
                     }
 
@@ -149,7 +149,7 @@ public class DefaultSubscriptionBaseService implements EventListener, Subscripti
         try {
             final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) dao.getSubscriptionFromId(event.getSubscriptionId(), context);
             if (subscription == null) {
-                log.warn("Failed to retrieve subscription for id %s", event.getSubscriptionId());
+                log.warn("Error retrieving subscriptionId='{}'", event.getSubscriptionId());
                 return;
             }
             if (subscription.getActiveVersion() > event.getActiveVersion()) {
@@ -176,9 +176,9 @@ public class DefaultSubscriptionBaseService implements EventListener, Subscripti
                 eventBus.post(busEvent);
             }
         } catch (final EventBusException e) {
-            log.warn("Failed to post subscription event " + event, e);
+            log.warn("Failed to post event {}", event, e);
         } catch (final CatalogApiException e) {
-            log.warn("Failed to post subscription event " + event, e);
+            log.warn("Failed to post event {}", event, e);
         }
     }
 
@@ -195,7 +195,7 @@ public class DefaultSubscriptionBaseService implements EventListener, Subscripti
                 return true;
             }
         } catch (final SubscriptionBaseError e) {
-            log.error(String.format("Failed to insert next phase for subscription %s", subscription.getId()), e);
+            log.warn("Error inserting next phase for subscriptionId='{}'", subscription.getId(), e);
         }
 
         return false;
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
index 16e6090..41c73a0 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
@@ -272,19 +272,19 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
             public UUID inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
                 final SubscriptionModelDao subscriptionModel = entitySqlDaoWrapperFactory.become(SubscriptionSqlDao.class).getById(subscriptionId.toString(), context);
                 if (subscriptionModel == null) {
-                    log.error(String.format(ErrorCode.SUB_INVALID_SUBSCRIPTION_ID.getFormat(), subscriptionId.toString()));
+                    log.warn(String.format(ErrorCode.SUB_INVALID_SUBSCRIPTION_ID.getFormat(), subscriptionId.toString()));
                     return null;
                 }
 
                 final UUID bundleId = subscriptionModel.getBundleId();
                 if (bundleId == null) {
-                    log.error(String.format(ErrorCode.SUB_GET_NO_BUNDLE_FOR_SUBSCRIPTION.getFormat(), subscriptionId.toString()));
+                    log.warn(String.format(ErrorCode.SUB_GET_NO_BUNDLE_FOR_SUBSCRIPTION.getFormat(), subscriptionId.toString()));
                     return null;
                 }
 
                 final SubscriptionBundleModelDao bundleModel = entitySqlDaoWrapperFactory.become(BundleSqlDao.class).getById(bundleId.toString(), context);
                 if (bundleModel == null) {
-                    log.error(String.format(ErrorCode.SUB_GET_INVALID_BUNDLE_ID.getFormat(), bundleId.toString()));
+                    log.warn(String.format(ErrorCode.SUB_GET_INVALID_BUNDLE_ID.getFormat(), bundleId.toString()));
                     return null;
                 }
                 return bundleModel.getAccountId();
@@ -1082,7 +1082,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
             final DefaultSubscriptionBase upToDateSubscription = createSubscriptionWithNewEvent(subscription, immediateEvent, context);
             notifyBusOfEffectiveImmediateChange(entitySqlDaoWrapperFactory, upToDateSubscription, immediateEvent, seqId, context);
         } catch (final CatalogApiException e) {
-            log.warn("Failed to post effective event for subscription " + subscription.getId(), e);
+            log.warn("Failed to post effective event for subscriptionId='{}'", subscription.getId(), e);
         }
     }
 
@@ -1098,7 +1098,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
 
             eventBus.postFromTransaction(busEvent, entitySqlDaoWrapperFactory.getHandle().getConnection());
         } catch (final EventBusException e) {
-            log.warn("Failed to post effective event for subscription " + subscription.getId(), e);
+            log.warn("Failed to post effective event for subscriptionId='{}'", subscription.getId(), e);
         }
     }
 
@@ -1107,7 +1107,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
         try {
             eventBus.postFromTransaction(new DefaultRequestedSubscriptionEvent(subscription, nextEvent, transitionType, context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()), entitySqlDaoWrapperFactory.getHandle().getConnection());
         } catch (final EventBusException e) {
-            log.warn("Failed to post requested change event for subscription " + subscription.getId(), e);
+            log.warn("Failed to post requested change event for subscriptionId='{}'", subscription.getId(), e);
         }
     }
 
@@ -1134,7 +1134,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
 
         final List<SubscriptionBundleModelDao> existingBundleModels = transBundleDao.getBundlesFromAccountAndKey(bundleData.getAccountId().toString(), bundleData.getExternalKey(), context);
         if (existingBundleModels.size() != 0) {
-            log.error(String.format("Attempted to create a bundle for account %s and key %s that already existed, skip...", bundleData.getAccountId().toString(), bundleData.getExternalKey()));
+            log.warn("Bundle already exists for accountId='{}', bundleExternalKey='{}'", bundleData.getAccountId(), bundleData.getExternalKey());
             return;
         }
 
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java b/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java
index a49f041..da95a45 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java
@@ -198,7 +198,7 @@ public class TenantCacheInvalidation {
                             try {
                                 parent.getEventBus().post(event);
                             } catch (final EventBusException e) {
-                                logger.warn("Failed post bus event " + event, e);
+                                logger.warn("Failed to post event {}", event, e);
                             }
                         }
                     } else {

util/pom.xml 30(+30 -0)

diff --git a/util/pom.xml b/util/pom.xml
index 67543c8..04d9e7b 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -111,6 +111,12 @@
             <artifactId>shiro-guice</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.flywaydb</groupId>
+            <artifactId>flyway-core</artifactId>
+            <version>4.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.jdbi</groupId>
             <artifactId>jdbi</artifactId>
         </dependency>
@@ -253,6 +259,30 @@
     <build>
         <plugins>
             <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>assemble-migrator</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <configuration>
+                            <finalName>killbill</finalName>
+                            <archive>
+                                <manifest>
+                                    <mainClass>org.killbill.billing.util.migration.Migrator</mainClass>
+                                </manifest>
+                            </archive>
+                        </configuration>
+                    </execution>
+                </executions>
+                <configuration>
+                    <descriptor>src/main/assembly/migrator.xml</descriptor>
+                </configuration>
+            </plugin>
+            <plugin>
                 <groupId>org.codehaus.mojo</groupId>
                 <artifactId>build-helper-maven-plugin</artifactId>
                 <executions>
diff --git a/util/src/main/assembly/migrator.xml b/util/src/main/assembly/migrator.xml
new file mode 100644
index 0000000..0e7eefe
--- /dev/null
+++ b/util/src/main/assembly/migrator.xml
@@ -0,0 +1,28 @@
+<assembly
+        xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
+    <id>flyway</id>
+    <formats>
+        <format>jar</format>
+    </formats>
+    <includeBaseDirectory>false</includeBaseDirectory>
+    <dependencySets>
+        <dependencySet>
+            <outputDirectory>/</outputDirectory>
+            <useProjectArtifact>true</useProjectArtifact>
+            <unpack>true</unpack>
+            <scope>test</scope>
+        </dependencySet>
+    </dependencySets>
+    <fileSets>
+        <fileSet>
+            <directory>${project.build.directory}/test-classes</directory>
+            <outputDirectory>/</outputDirectory>
+            <includes>
+                <include>**/*.class</include>
+            </includes>
+            <useDefaultExcludes>true</useDefaultExcludes>
+        </fileSet>
+    </fileSets>
+</assembly>
diff --git a/util/src/main/java/org/killbill/billing/util/broadcast/DefaultBroadcastApi.java b/util/src/main/java/org/killbill/billing/util/broadcast/DefaultBroadcastApi.java
index 7c24739..0a1a922 100644
--- a/util/src/main/java/org/killbill/billing/util/broadcast/DefaultBroadcastApi.java
+++ b/util/src/main/java/org/killbill/billing/util/broadcast/DefaultBroadcastApi.java
@@ -51,7 +51,7 @@ public class DefaultBroadcastApi implements BroadcastApi {
             try {
                 eventBus.post(busEvent);
             } catch (final EventBusException e) {
-                logger.warn("Failed to deliver bus event ", e);
+                logger.warn("Failed to post event {}", event, e);
             }
         } else {
             final BroadcastModelDao modelDao = new BroadcastModelDao(serviceName, type, event, createdDate, createdBy);
diff --git a/util/src/main/java/org/killbill/billing/util/broadcast/DefaultBroadcastService.java b/util/src/main/java/org/killbill/billing/util/broadcast/DefaultBroadcastService.java
index 7ee1a99..a569fff 100644
--- a/util/src/main/java/org/killbill/billing/util/broadcast/DefaultBroadcastService.java
+++ b/util/src/main/java/org/killbill/billing/util/broadcast/DefaultBroadcastService.java
@@ -93,7 +93,7 @@ public class DefaultBroadcastService implements BroadcastService {
             broadcastExecutor.shutdown();
             boolean success = broadcastExecutor.awaitTermination(TERMINATION_TIMEOUT_SEC, TimeUnit.SECONDS);
             if (!success) {
-                logger.warn("BroadcastExecutor failed to complete termination within " + TERMINATION_TIMEOUT_SEC + "sec");
+                logger.warn("BroadcastExecutor failed to complete termination within {} sec", TERMINATION_TIMEOUT_SEC);
             }
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
@@ -139,11 +139,11 @@ public class DefaultBroadcastService implements BroadcastService {
                     return;
                 }
 
+                final BroadcastInternalEvent event = new DefaultBroadcastInternalEvent(cur.getServiceName(), cur.getType(), cur.getEvent());
                 try {
-                    final BroadcastInternalEvent event = new DefaultBroadcastInternalEvent(cur.getServiceName(), cur.getType(), cur.getEvent());
                     eventBus.post(event);
                 } catch (final EventBusException e) {
-                    logger.error("Failed to send event BroadcastInternalEvent: ", e);
+                    logger.warn("Failed to post event {}", event, e);
                 } finally {
                     parent.setLatestRecordIdProcessed(cur.getRecordId());
                 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/TenantCatalogCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/TenantCatalogCacheLoader.java
index 897e75d..b3f9305 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/TenantCatalogCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/TenantCatalogCacheLoader.java
@@ -72,11 +72,10 @@ public class TenantCatalogCacheLoader extends BaseCacheLoader {
             return null;
         }
         try {
-            log.info("Loading catalog cache for tenant " + internalTenantContext.getTenantRecordId());
+            log.info("Loading catalog cache for tenantRecordId='{}'", internalTenantContext.getTenantRecordId());
             return callback.loadCatalog(catalogXMLs, tenantRecordId);
         } catch (final CatalogApiException e) {
-            throw new IllegalStateException(String.format("Failed to de-serialize catalog for tenant %s : %s",
-                                                          internalTenantContext.getTenantRecordId(), e.getMessage()), e);
+            throw new IllegalStateException(String.format("Failed to de-serialize catalog for tenantRecordId='%s'", internalTenantContext.getTenantRecordId()), e);
         }
     }
 
diff --git a/util/src/main/java/org/killbill/billing/util/callcontext/CallContextFactory.java b/util/src/main/java/org/killbill/billing/util/callcontext/CallContextFactory.java
index 324a610..3fb5188 100644
--- a/util/src/main/java/org/killbill/billing/util/callcontext/CallContextFactory.java
+++ b/util/src/main/java/org/killbill/billing/util/callcontext/CallContextFactory.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2011 Ning, Inc.
+ * Copyright 2010-2014 Ning, Inc.
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -20,18 +22,10 @@ import java.util.UUID;
 
 import javax.annotation.Nullable;
 
-import org.joda.time.DateTime;
-
 public interface CallContextFactory {
 
     TenantContext createTenantContext(@Nullable UUID tenantId);
 
-    CallContext createCallContext(@Nullable UUID tenantId, String userName, CallOrigin callOrigin, UserType userType, UUID userToken);
-
     CallContext createCallContext(@Nullable UUID tenantId, String userName, CallOrigin callOrigin, UserType userType,
                                   String reasonCode, String comment, UUID userToken);
-
-    CallContext createCallContext(@Nullable UUID tenantId, String userName, CallOrigin callOrigin, UserType userType);
-
-    CallContext toMigrationCallContext(@Nullable CallContext callContext, DateTime createdDate, DateTime updatedDate);
 }
diff --git a/util/src/main/java/org/killbill/billing/util/callcontext/DefaultCallContextFactory.java b/util/src/main/java/org/killbill/billing/util/callcontext/DefaultCallContextFactory.java
index a8f9b22..30ba050 100644
--- a/util/src/main/java/org/killbill/billing/util/callcontext/DefaultCallContextFactory.java
+++ b/util/src/main/java/org/killbill/billing/util/callcontext/DefaultCallContextFactory.java
@@ -20,8 +20,6 @@ import java.util.UUID;
 
 import javax.annotation.Nullable;
 
-import org.joda.time.DateTime;
-
 import org.killbill.billing.callcontext.DefaultCallContext;
 import org.killbill.billing.callcontext.DefaultTenantContext;
 import org.killbill.clock.Clock;
@@ -44,23 +42,7 @@ public class DefaultCallContextFactory implements CallContextFactory {
 
     @Override
     public CallContext createCallContext(@Nullable final UUID tenantId, final String userName, final CallOrigin callOrigin,
-                                         final UserType userType, @Nullable final UUID userToken) {
-        return new DefaultCallContext(tenantId, userName, callOrigin, userType, userToken, clock);
-    }
-
-    @Override
-    public CallContext createCallContext(@Nullable final UUID tenantId, final String userName, final CallOrigin callOrigin,
                                          final UserType userType, final String reasonCode, final String comment, final UUID userToken) {
         return new DefaultCallContext(tenantId, userName, callOrigin, userType, reasonCode, comment, userToken, clock);
     }
-
-    @Override
-    public CallContext createCallContext(@Nullable final UUID tenantId, final String userName, final CallOrigin callOrigin, final UserType userType) {
-        return createCallContext(tenantId, userName, callOrigin, userType, null);
-    }
-
-    @Override
-    public CallContext toMigrationCallContext(final CallContext callContext, final DateTime createdDate, final DateTime updatedDate) {
-        return new MigrationCallContext(callContext, createdDate, updatedDate);
-    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/callcontext/InternalCallContextFactory.java b/util/src/main/java/org/killbill/billing/util/callcontext/InternalCallContextFactory.java
index 0460fd4..c16cd2e 100644
--- a/util/src/main/java/org/killbill/billing/util/callcontext/InternalCallContextFactory.java
+++ b/util/src/main/java/org/killbill/billing/util/callcontext/InternalCallContextFactory.java
@@ -36,6 +36,7 @@ import org.killbill.billing.util.cache.Cachable.CacheType;
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.billing.util.dao.NonEntityDao;
 import org.killbill.clock.Clock;
+import org.slf4j.MDC;
 
 import com.google.common.base.MoreObjects;
 
@@ -44,6 +45,9 @@ public class InternalCallContextFactory {
 
     public static final long INTERNAL_TENANT_RECORD_ID = 0L;
 
+    public static final String MDC_KB_ACCOUNT_RECORD_ID = "kb.accountRecordId";
+    public static final String MDC_KB_TENANT_RECORD_ID = "kb.tenantRecordId";
+
     private final ImmutableAccountInternalApi accountInternalApi;
     private final Clock clock;
     private final NonEntityDao nonEntityDao;
@@ -130,6 +134,8 @@ public class InternalCallContextFactory {
      * @return internal tenant callcontext
      */
     public InternalTenantContext createInternalTenantContext(final Long tenantRecordId, @Nullable final Long accountRecordId) {
+        populateMDCContext(accountRecordId, tenantRecordId);
+
         if (accountRecordId == null) {
             return new InternalTenantContext(tenantRecordId);
         } else {
@@ -212,12 +218,18 @@ public class InternalCallContextFactory {
     public InternalCallContext createInternalCallContext(final CallContext context) {
         // If tenant id is null, this will default to the default tenant record id (multi-tenancy disabled)
         final Long tenantRecordId = getTenantRecordIdSafe(context);
+
+        populateMDCContext(null, tenantRecordId);
+
         return new InternalCallContext(tenantRecordId, context);
     }
 
     // Used when we need to re-hydrate the callcontext with the account_record_id (when creating the account)
     public InternalCallContext createInternalCallContext(final Long accountRecordId, final InternalCallContext context) {
         final DateTimeZone accountTimeZone = getAccountTimeZone(context.getTenantRecordId(), accountRecordId);
+
+        populateMDCContext(accountRecordId, context.getTenantRecordId());
+
         return new InternalCallContext(context, accountRecordId, accountTimeZone);
     }
 
@@ -238,15 +250,26 @@ public class InternalCallContextFactory {
         final Long nonNulTenantRecordId = MoreObjects.firstNonNull(tenantRecordId, INTERNAL_TENANT_RECORD_ID);
         final DateTimeZone accountTimeZone = getAccountTimeZone(tenantRecordId, accountRecordId);
 
+        populateMDCContext(accountRecordId, nonNulTenantRecordId);
+
         return new InternalCallContext(nonNulTenantRecordId, accountRecordId, accountTimeZone, userToken, userName, callOrigin, userType, reasonCode, comment,
                                        createdDate, updatedDate);
     }
 
+    private void populateMDCContext(@Nullable final Long accountRecordId, final Long tenantRecordId) {
+        if (accountRecordId != null) {
+            MDC.put(MDC_KB_ACCOUNT_RECORD_ID, String.valueOf(accountRecordId));
+        }
+        MDC.put(MDC_KB_TENANT_RECORD_ID, String.valueOf(tenantRecordId));
+    }
+
     private DateTimeZone getAccountTimeZone(final Long tenantRecordId, @Nullable final Long accountRecordId) {
         if (accountRecordId == null || accountInternalApi == null) {
             return null;
         }
 
+        populateMDCContext(accountRecordId, tenantRecordId);
+
         final InternalTenantContext tmp = new InternalTenantContext(tenantRecordId, accountRecordId, null);
 
         try {
diff --git a/util/src/main/java/org/killbill/billing/util/customfield/dao/DefaultCustomFieldDao.java b/util/src/main/java/org/killbill/billing/util/customfield/dao/DefaultCustomFieldDao.java
index 1416fb4..6561b8f 100644
--- a/util/src/main/java/org/killbill/billing/util/customfield/dao/DefaultCustomFieldDao.java
+++ b/util/src/main/java/org/killbill/billing/util/customfield/dao/DefaultCustomFieldDao.java
@@ -146,7 +146,7 @@ public class DefaultCustomFieldDao extends EntityDaoBase<CustomFieldModelDao, Cu
         try {
             bus.postFromTransaction(customFieldEvent, entitySqlDaoWrapperFactory.getHandle().getConnection());
         } catch (final PersistentBus.EventBusException e) {
-            log.warn("Failed to post tag event for custom field " + customField.getId().toString(), e);
+            log.warn("Failed to post tag event for customFieldId='{}'", customField.getId().toString(), e);
         }
 
     }
diff --git a/util/src/main/java/org/killbill/billing/util/email/DefaultEmailSender.java b/util/src/main/java/org/killbill/billing/util/email/DefaultEmailSender.java
index 7c5c8f2..7f8bac4 100644
--- a/util/src/main/java/org/killbill/billing/util/email/DefaultEmailSender.java
+++ b/util/src/main/java/org/killbill/billing/util/email/DefaultEmailSender.java
@@ -89,7 +89,7 @@ public class DefaultEmailSender implements EmailSender {
 
             email.setSSL(config.useSSL());
 
-            log.info("Sending email to {}, cc {}, subject {}", new Object[]{to, cc, subject});
+            log.info("Sending email to='{}', cc='{}', subject='{}'", to, cc, subject);
             email.send();
         } catch (EmailException ee) {
             throw new EmailApiException(ee, ErrorCode.EMAIL_SENDING_FAILED);
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationHelper.java b/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationHelper.java
index a417357..d3ffa24 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationHelper.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationHelper.java
@@ -74,7 +74,7 @@ public class DefaultPaginationHelper {
                     maxNbRecords = Math.max(maxNbRecords, pages.getMaxNbRecords());
                 }
             } catch (final BillingExceptionBase e) {
-                log.warn("Error while searching plugin " + pluginName, e);
+                log.warn("Error while searching plugin='{}'", pluginName, e);
                 // Non-fatal, continue to search other plugins
             }
         }
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java
index 1c7c00f..ddbd3bb 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java
@@ -137,7 +137,7 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
             systemLdapCtx = ldapContextFactory.getSystemLdapContext();
             return findLDAPGroupsForUser(username, systemLdapCtx);
         } catch (AuthenticationException ex) {
-            log.info("LDAP authentication exception: " + ex.getLocalizedMessage());
+            log.info("LDAP authentication exception='{}'", ex.getLocalizedMessage());
             return ImmutableSet.<String>of();
         } finally {
             LdapUtils.closeContext(systemLdapCtx);
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDao.java
index 62543e5..ed2d38c 100644
--- a/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDao.java
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDao.java
@@ -142,7 +142,7 @@ public class DefaultTagDao extends EntityDaoBase<TagModelDao, Tag, TagApiExcepti
         try {
             bus.postFromTransaction(tagEvent, entitySqlDaoWrapperFactory.getHandle().getConnection());
         } catch (final PersistentBus.EventBusException e) {
-            log.warn("Failed to post tag event for tag " + tag.getId().toString(), e);
+            log.warn("Failed to post tag event for tagId='{}'", tag.getId().toString(), e);
         }
     }
 
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java
index 9039976..46128ce 100644
--- a/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java
@@ -184,7 +184,7 @@ public class DefaultTagDefinitionDao extends EntityDaoBase<TagDefinitionModelDao
                     try {
                         bus.postFromTransaction(tagDefinitionEvent, entitySqlDaoWrapperFactory.getHandle().getConnection());
                     } catch (final PersistentBus.EventBusException e) {
-                        log.warn("Failed to post tag definition creation event for tag " + tagDefinition.getId(), e);
+                        log.warn("Failed to post tag definition creation event for tagDefinitionId='{}'", tagDefinition.getId(), e);
                     }
 
                     return tagDefinition;
@@ -263,7 +263,7 @@ public class DefaultTagDefinitionDao extends EntityDaoBase<TagDefinitionModelDao
         try {
             bus.postFromTransaction(tagDefinitionEvent, entitySqlDaoWrapperFactory.getHandle().getConnection());
         } catch (final PersistentBus.EventBusException e) {
-            log.warn("Failed to post tag definition event for tag " + tagDefinition.getId().toString(), e);
+            log.warn("Failed to post tag definition event for tagDefinitionId='{}'", tagDefinition.getId().toString(), e);
         }
     }
 
diff --git a/util/src/test/java/org/flywaydb/core/FlywayWithDryRun.java b/util/src/test/java/org/flywaydb/core/FlywayWithDryRun.java
new file mode 100644
index 0000000..0cf27a2
--- /dev/null
+++ b/util/src/test/java/org/flywaydb/core/FlywayWithDryRun.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.flywaydb.core;
+
+import java.sql.Connection;
+import java.util.List;
+
+import org.flywaydb.core.api.FlywayException;
+import org.flywaydb.core.api.callback.FlywayCallback;
+import org.flywaydb.core.api.resolver.MigrationResolver;
+import org.flywaydb.core.internal.dbsupport.DbSupport;
+import org.flywaydb.core.internal.dbsupport.Schema;
+import org.flywaydb.core.internal.dbsupport.SqlStatement;
+import org.flywaydb.core.internal.dbsupport.Table;
+import org.flywaydb.core.internal.metadatatable.MetaDataTable;
+import org.flywaydb.core.internal.util.PlaceholderReplacer;
+import org.killbill.billing.util.migration.DbMigrateWithDryRun;
+
+public class FlywayWithDryRun extends Flyway {
+
+    private final List<SqlStatement> sqlStatements;
+
+    public FlywayWithDryRun(final List<SqlStatement> sqlStatements) {
+        this.sqlStatements = sqlStatements;
+    }
+
+    // Note: we assume the schemas have already been created and baseline() has already been called
+    public int dryRunMigrate() throws FlywayException {
+        final PlaceholderReplacer placeholderReplacer = new PlaceholderReplacer(getPlaceholders(),
+                                                                                getPlaceholderPrefix(),
+                                                                                getPlaceholderSuffix());
+        return execute(new Command<Integer>() {
+            public Integer execute(final Connection connectionMetaDataTable,
+                                   final Connection connectionUserObjects,
+                                   final MigrationResolver migrationResolver,
+                                   final MetaDataTable metaDataTable,
+                                   final DbSupport dbSupport,
+                                   final Schema[] schemas,
+                                   final FlywayCallback[] flywayCallbacks) {
+                final Table metaDataDBTable = schemas[0].getTable(getTable());
+
+                final DbMigrateWithDryRun dbMigrate = new DbMigrateWithDryRun(sqlStatements,
+                                                                              placeholderReplacer,
+                                                                              getEncoding(),
+                                                                              metaDataDBTable,
+                                                                              connectionMetaDataTable,
+                                                                              connectionUserObjects,
+                                                                              dbSupport,
+                                                                              metaDataTable,
+                                                                              schemas[0],
+                                                                              migrationResolver,
+                                                                              getTarget(),
+                                                                              isIgnoreFutureMigrations(),
+                                                                              false,
+                                                                              isOutOfOrder(),
+                                                                              flywayCallbacks);
+                return dbMigrate.dryRunMigrate();
+            }
+        });
+    }
+}
diff --git a/util/src/test/java/org/killbill/billing/api/TestApiListener.java b/util/src/test/java/org/killbill/billing/api/TestApiListener.java
index d710a92..90d142d 100644
--- a/util/src/test/java/org/killbill/billing/api/TestApiListener.java
+++ b/util/src/test/java/org/killbill/billing/api/TestApiListener.java
@@ -41,7 +41,6 @@ import org.killbill.billing.events.NullInvoiceInternalEvent;
 import org.killbill.billing.events.PaymentErrorInternalEvent;
 import org.killbill.billing.events.PaymentInfoInternalEvent;
 import org.killbill.billing.events.PaymentPluginErrorInternalEvent;
-import org.killbill.billing.events.RepairSubscriptionInternalEvent;
 import org.killbill.billing.events.TagDefinitionInternalEvent;
 import org.killbill.billing.events.TagInternalEvent;
 import org.skife.jdbi.v2.Handle;
@@ -139,13 +138,6 @@ public class TestApiListener {
 
 
     @Subscribe
-    public void handleRepairSubscriptionEvents(final RepairSubscriptionInternalEvent event) {
-        log.info(String.format("Got RepairSubscriptionEvent event %s", event.toString()));
-        assertEqualsNicely(NextEvent.REPAIR_BUNDLE);
-        notifyIfStackEmpty();
-    }
-
-    @Subscribe
     public void handleEntitlementEvents(final EffectiveEntitlementInternalEvent eventEffective) {
         log.info(String.format("Got entitlement event %s", eventEffective.toString()));
         switch (eventEffective.getTransitionType()) {
diff --git a/util/src/test/java/org/killbill/billing/util/migration/CapturingMetaDataTable.java b/util/src/test/java/org/killbill/billing/util/migration/CapturingMetaDataTable.java
new file mode 100644
index 0000000..164a58a
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/migration/CapturingMetaDataTable.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.migration;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.flywaydb.core.api.MigrationVersion;
+import org.flywaydb.core.internal.dbsupport.DbSupport;
+import org.flywaydb.core.internal.dbsupport.SqlStatement;
+import org.flywaydb.core.internal.dbsupport.Table;
+import org.flywaydb.core.internal.metadatatable.AppliedMigration;
+import org.flywaydb.core.internal.metadatatable.MetaDataTableImpl;
+
+public class CapturingMetaDataTable extends MetaDataTableImpl {
+
+    private final List<SqlStatement> sqlStatements;
+    private final DbSupport dbSupport;
+    private final Table table;
+
+    /**
+     * Creates a new instance of the metadata table support.
+     *
+     * @param sqlStatements The current list of all pending migrations.
+     * @param dbSupport     Database-specific functionality.
+     * @param table         The metadata table used by flyway.
+     */
+    public CapturingMetaDataTable(final List<SqlStatement> sqlStatements, final DbSupport dbSupport, final Table table) {
+        super(dbSupport, table);
+        this.sqlStatements = sqlStatements;
+        this.dbSupport = dbSupport;
+        this.table = table;
+    }
+
+    @Override
+    public void addAppliedMigration(final AppliedMigration appliedMigration) {
+        final MigrationVersion version = appliedMigration.getVersion();
+        final String versionStr = version == null ? null : version.toString();
+        final int calculateInstalledRank;
+        try {
+            calculateInstalledRank = calculateInstalledRank();
+        } catch (final SQLException e) {
+            throw new RuntimeException(e);
+        }
+
+        final String sql = new StringBuilder().append("INSERT INTO ")
+                                              .append(table)
+                                              .append(" (")
+                                              .append(dbSupport.quote("installed_rank")).append(",")
+                                              .append(dbSupport.quote("version")).append(",")
+                                              .append(dbSupport.quote("description")).append(",")
+                                              .append(dbSupport.quote("type")).append(",")
+                                              .append(dbSupport.quote("script")).append(",")
+                                              .append(dbSupport.quote("checksum")).append(",")
+                                              .append(dbSupport.quote("installed_by")).append(",")
+                                              .append(dbSupport.quote("execution_time")).append(",")
+                                              .append(dbSupport.quote("success"))
+                                              .append(")")
+                                              .append(" VALUES (")
+                                              .append(calculateInstalledRank + appliedMigration.getInstalledRank()).append(",")
+                                              .append("'").append(versionStr).append("',")
+                                              .append("'").append(appliedMigration.getDescription()).append("',")
+                                              .append("'").append(appliedMigration.getType().name()).append("',")
+                                              .append("'").append(appliedMigration.getScript()).append("',")
+                                              .append(appliedMigration.getChecksum()).append(",")
+                                              .append(dbSupport.getCurrentUserFunction()).append(",")
+                                              .append(appliedMigration.getExecutionTime()).append(",")
+                                              .append(appliedMigration.isSuccess())
+                                              .append(")")
+                                              .toString();
+
+        sqlStatements.add(new SqlStatement(0, sql, false));
+    }
+
+    /**
+     * Calculates the installed rank for the new migration to be inserted.
+     *
+     * @return The installed rank.
+     */
+    private int calculateInstalledRank() throws SQLException {
+        final int currentMax = dbSupport.getJdbcTemplate().queryForInt("SELECT MAX(" + dbSupport.quote("installed_rank") + ")" + " FROM " + table);
+        return currentMax + 1;
+    }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/migration/CapturingSqlMigrationExecutor.java b/util/src/test/java/org/killbill/billing/util/migration/CapturingSqlMigrationExecutor.java
new file mode 100644
index 0000000..6360b75
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/migration/CapturingSqlMigrationExecutor.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.migration;
+
+import java.sql.Connection;
+import java.util.List;
+
+import org.flywaydb.core.api.resolver.MigrationExecutor;
+import org.flywaydb.core.internal.dbsupport.DbSupport;
+import org.flywaydb.core.internal.dbsupport.SqlScript;
+import org.flywaydb.core.internal.dbsupport.SqlStatement;
+import org.flywaydb.core.internal.util.PlaceholderReplacer;
+import org.flywaydb.core.internal.util.scanner.Resource;
+
+public class CapturingSqlMigrationExecutor implements MigrationExecutor {
+
+    private final List<SqlStatement> sqlStatements;
+    private final DbSupport dbSupport;
+    private final PlaceholderReplacer placeholderReplacer;
+    private final Resource sqlScriptResource;
+    private final String encoding;
+
+    /**
+     * Creates a new sql script migration based on this sql script.
+     *
+     * @param sqlStatements       The current list of all pending migrations.
+     * @param dbSupport           The database-specific support.
+     * @param sqlScriptResource   The resource containing the sql script.
+     * @param placeholderReplacer The placeholder replacer to apply to sql migration scripts.
+     * @param encoding            The encoding of this Sql migration.
+     */
+    public CapturingSqlMigrationExecutor(final List<SqlStatement> sqlStatements,
+                                         final DbSupport dbSupport,
+                                         final Resource sqlScriptResource,
+                                         final PlaceholderReplacer placeholderReplacer,
+                                         final String encoding) {
+        this.sqlStatements = sqlStatements;
+        this.dbSupport = dbSupport;
+        this.sqlScriptResource = sqlScriptResource;
+        this.encoding = encoding;
+        this.placeholderReplacer = placeholderReplacer;
+    }
+
+    @Override
+    public void execute(final Connection connection) {
+        final SqlScript sqlScript = new SqlScript(dbSupport, sqlScriptResource, placeholderReplacer, encoding);
+        for (final SqlStatement sqlStatement : sqlScript.getSqlStatements()) {
+            sqlStatements.add(sqlStatement);
+        }
+    }
+
+    @Override
+    public boolean executeInTransaction() {
+        return true;
+    }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/migration/DbMigrateWithDryRun.java b/util/src/test/java/org/killbill/billing/util/migration/DbMigrateWithDryRun.java
new file mode 100644
index 0000000..7b81a39
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/migration/DbMigrateWithDryRun.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.migration;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.List;
+
+import org.flywaydb.core.api.FlywayException;
+import org.flywaydb.core.api.MigrationInfo;
+import org.flywaydb.core.api.MigrationVersion;
+import org.flywaydb.core.api.callback.FlywayCallback;
+import org.flywaydb.core.api.resolver.MigrationExecutor;
+import org.flywaydb.core.api.resolver.MigrationResolver;
+import org.flywaydb.core.internal.command.DbMigrate;
+import org.flywaydb.core.internal.dbsupport.DbSupport;
+import org.flywaydb.core.internal.dbsupport.DbSupportFactory;
+import org.flywaydb.core.internal.dbsupport.Schema;
+import org.flywaydb.core.internal.dbsupport.SqlStatement;
+import org.flywaydb.core.internal.dbsupport.Table;
+import org.flywaydb.core.internal.info.MigrationInfoImpl;
+import org.flywaydb.core.internal.info.MigrationInfoServiceImpl;
+import org.flywaydb.core.internal.metadatatable.AppliedMigration;
+import org.flywaydb.core.internal.metadatatable.MetaDataTable;
+import org.flywaydb.core.internal.util.PlaceholderReplacer;
+import org.flywaydb.core.internal.util.jdbc.TransactionCallback;
+import org.flywaydb.core.internal.util.jdbc.TransactionTemplate;
+import org.flywaydb.core.internal.util.logging.Log;
+import org.flywaydb.core.internal.util.logging.LogFactory;
+import org.flywaydb.core.internal.util.scanner.filesystem.FileSystemResource;
+
+public class DbMigrateWithDryRun extends DbMigrate {
+
+    private static final Log LOG = LogFactory.getLog(DbMigrateWithDryRun.class);
+
+    private final List<SqlStatement> sqlStatements;
+    private final PlaceholderReplacer placeholderReplacer;
+    private final String encoding;
+    private final MigrationVersion target;
+    private final DbSupport dbSupport;
+    private final MetaDataTable metaDataTableForDryRun;
+    private final Schema schema;
+    private final MigrationResolver migrationResolver;
+    private final Connection connectionMetaDataTable;
+    private final Connection connectionUserObjects;
+    private final boolean outOfOrder;
+    private final FlywayCallback[] callbacks;
+    private final DbSupport dbSupportUserObjects;
+
+    /**
+     * Creates a new database migrator.
+     *
+     * @param sqlStatements               The current list of all pending migrations.
+     * @param placeholderReplacer         The placeholder replacer to apply to sql migration scripts.
+     * @param encoding                    The encoding of Sql migrations.
+     * @param metaDataDBTable             The database metadata DB Table.
+     * @param connectionMetaDataTable     The connection to use.
+     * @param connectionUserObjects       The connection to use to perform the actual database migrations.
+     * @param dbSupport                   Database-specific functionality.
+     * @param metaDataTable               The database metadata table.
+     * @param migrationResolver           The migration resolver.
+     * @param target                      The target version of the migration.
+     * @param ignoreFutureMigrations      Flag whether to ignore future migrations or not.
+     * @param ignoreFailedFutureMigration Flag whether to ignore failed future migrations or not.
+     * @param outOfOrder                  Allows migrations to be run "out of order".
+     */
+    public DbMigrateWithDryRun(final List<SqlStatement> sqlStatements,
+                               final PlaceholderReplacer placeholderReplacer,
+                               final String encoding,
+                               final Table metaDataDBTable,
+                               final Connection connectionMetaDataTable,
+                               final Connection connectionUserObjects,
+                               final DbSupport dbSupport,
+                               final MetaDataTable metaDataTable,
+                               final Schema schema,
+                               final MigrationResolver migrationResolver,
+                               final MigrationVersion target,
+                               final boolean ignoreFutureMigrations,
+                               final boolean ignoreFailedFutureMigration,
+                               final boolean outOfOrder,
+                               final FlywayCallback[] callbacks) {
+        super(connectionMetaDataTable, connectionUserObjects, dbSupport, metaDataTable, schema, migrationResolver, target, ignoreFutureMigrations, ignoreFailedFutureMigration, outOfOrder, callbacks);
+        this.sqlStatements = sqlStatements;
+        this.placeholderReplacer = placeholderReplacer;
+        this.encoding = encoding;
+        this.connectionMetaDataTable = connectionMetaDataTable;
+        this.connectionUserObjects = connectionUserObjects;
+        this.dbSupport = dbSupport;
+        this.schema = schema;
+        this.migrationResolver = migrationResolver;
+        this.target = target;
+        this.outOfOrder = outOfOrder;
+        this.callbacks = callbacks;
+
+        this.dbSupportUserObjects = DbSupportFactory.createDbSupport(connectionUserObjects, false);
+
+        // PIERRE: change MetaDataTable to capture the SQL
+        this.metaDataTableForDryRun = new CapturingMetaDataTable(sqlStatements, dbSupport, metaDataDBTable);
+    }
+
+    public int dryRunMigrate() throws FlywayException {
+        try {
+            for (final FlywayCallback callback : callbacks) {
+                new TransactionTemplate(connectionUserObjects).execute(new TransactionCallback<Object>() {
+                    @Override
+                    public Object doInTransaction() throws SQLException {
+                        dbSupportUserObjects.changeCurrentSchemaTo(schema);
+                        callback.beforeMigrate(connectionUserObjects);
+                        return null;
+                    }
+                });
+            }
+
+            // PIERRE: perform a single query to the metadata table
+            final MigrationInfoServiceImpl infoService = new MigrationInfoServiceImpl(migrationResolver, metaDataTableForDryRun, target, outOfOrder, true, true);
+            infoService.refresh();
+
+            final MigrationInfoImpl[] pendingMigrations = infoService.pending();
+            new TransactionTemplate(connectionMetaDataTable, false).execute(new TransactionCallback<Boolean>() {
+                public Boolean doInTransaction() {
+                    int i = 1;
+                    for (final MigrationInfoImpl migrationInfo : pendingMigrations) {
+                        applyMigration(i, migrationInfo);
+                        i++;
+                    }
+
+                    return true;
+                }
+            });
+
+            for (final FlywayCallback callback : callbacks) {
+                new TransactionTemplate(connectionUserObjects).execute(new TransactionCallback<Object>() {
+                    @Override
+                    public Object doInTransaction() throws SQLException {
+                        dbSupportUserObjects.changeCurrentSchemaTo(schema);
+                        callback.afterMigrate(connectionUserObjects);
+                        return null;
+                    }
+                });
+            }
+
+            return pendingMigrations.length;
+        } finally {
+            dbSupportUserObjects.restoreCurrentSchema();
+        }
+    }
+
+    private void applyMigration(final int installedRnk, final MigrationInfoImpl migration) {
+        final MigrationVersion version = migration.getVersion();
+        final String migrationText;
+        if (version != null) {
+            migrationText = "schema " + schema + " to version " + version + " - " + migration.getDescription();
+        } else {
+            migrationText = "schema " + schema + " with repeatable migration " + migration.getDescription();
+        }
+        LOG.info("Migrating " + migrationText);
+
+        // PIERRE: override the executor to capture the SQL
+        final FileSystemResource sqlScriptResource = new FileSystemResource(migration.getResolvedMigration().getPhysicalLocation());
+        final MigrationExecutor migrationExecutor = new CapturingSqlMigrationExecutor(sqlStatements,
+                                                                                      dbSupport,
+                                                                                      sqlScriptResource,
+                                                                                      placeholderReplacer,
+                                                                                      encoding);
+        try {
+            doMigrate(migration, migrationExecutor, migrationText);
+        } catch (final SQLException e) {
+            throw new FlywayException("Unable to apply migration", e);
+        }
+
+        final AppliedMigration appliedMigration = new AppliedMigration(installedRnk,
+                                                                       version,
+                                                                       migration.getDescription(),
+                                                                       migration.getType(),
+                                                                       migration.getScript(),
+                                                                       migration.getResolvedMigration().getChecksum(),
+                                                                       null,
+                                                                       null,
+                                                                       -1,
+                                                                       true);
+        metaDataTableForDryRun.addAppliedMigration(appliedMigration);
+    }
+
+    private void doMigrate(final MigrationInfo migration, final MigrationExecutor migrationExecutor, final String migrationText) throws SQLException {
+        for (final FlywayCallback callback : callbacks) {
+            dbSupportUserObjects.changeCurrentSchemaTo(schema);
+            callback.beforeEachMigrate(connectionUserObjects, migration);
+        }
+
+        dbSupportUserObjects.changeCurrentSchemaTo(schema);
+        migrationExecutor.execute(connectionUserObjects);
+        LOG.debug("Successfully completed migration of " + migrationText);
+
+        for (final FlywayCallback callback : callbacks) {
+            dbSupportUserObjects.changeCurrentSchemaTo(schema);
+            callback.afterEachMigrate(connectionUserObjects, migration);
+        }
+    }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/migration/Migrator.java b/util/src/test/java/org/killbill/billing/util/migration/Migrator.java
new file mode 100644
index 0000000..0b5b6e8
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/migration/Migrator.java
@@ -0,0 +1,540 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.migration;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.flywaydb.core.FlywayWithDryRun;
+import org.flywaydb.core.api.FlywayException;
+import org.flywaydb.core.internal.dbsupport.SqlStatement;
+import org.flywaydb.core.internal.info.MigrationInfoDumper;
+import org.flywaydb.core.internal.util.ClassUtils;
+import org.flywaydb.core.internal.util.FileCopyUtils;
+import org.flywaydb.core.internal.util.StringUtils;
+import org.flywaydb.core.internal.util.VersionPrinter;
+import org.flywaydb.core.internal.util.logging.Log;
+import org.flywaydb.core.internal.util.logging.LogFactory;
+import org.flywaydb.core.internal.util.logging.console.ConsoleLog.Level;
+import org.flywaydb.core.internal.util.logging.console.ConsoleLogCreator;
+import org.flywaydb.core.internal.util.scanner.classpath.ClassPathResource;
+
+// Copied over from org.flywaydb.commandline.Main (not easily extensible unfortunately) to support dry-run
+public class Migrator {
+
+    /**
+     * The property name for the directory containing a list of jars to load on the classpath.
+     */
+    private static final String PROPERTY_JAR_DIRS = "flyway.jarDirs";
+    private static Log LOG;
+
+    /**
+     * Initializes the logging.
+     *
+     * @param level The minimum level to log at.
+     */
+    private static void initLogging(final Level level) {
+        LogFactory.setLogCreator(new ConsoleLogCreator(level));
+        LOG = LogFactory.getLog(Migrator.class);
+    }
+
+    /**
+     * Main method.
+     *
+     * @param args The command-line arguments.
+     */
+    public static void main(final String[] args) {
+        final Level logLevel = getLogLevel(args);
+        initLogging(logLevel);
+
+        try {
+            if (isPrintVersionAndExit(args)) {
+                printVersion();
+                System.exit(0);
+            }
+
+            final List<String> operations = determineOperations(args);
+            if (operations.isEmpty()) {
+                printUsage();
+                return;
+            }
+
+            final Properties properties = new Properties();
+            initializeDefaults(properties);
+            loadConfiguration(properties, args);
+            overrideConfiguration(properties, args);
+            dumpConfiguration(properties);
+
+            loadJdbcDrivers();
+            loadJavaMigrationsFromJarDirs(properties);
+
+            final List<SqlStatement> sqlStatements = new LinkedList<SqlStatement>();
+            final FlywayWithDryRun flyway = new FlywayWithDryRun(sqlStatements);
+            filterProperties(properties);
+            flyway.configure(properties);
+
+            for (final String operation : operations) {
+                executeOperation(flyway, operation, sqlStatements);
+            }
+        } catch (final Exception e) {
+            if (logLevel == Level.DEBUG) {
+                LOG.error("Unexpected error", e);
+            } else {
+                if (e instanceof FlywayException) {
+                    LOG.error(e.getMessage());
+                } else {
+                    LOG.error(e.toString());
+                }
+            }
+            System.exit(1);
+        }
+    }
+
+    private static boolean isPrintVersionAndExit(final String[] args) {
+        for (final String arg : args) {
+            if ("-v".equals(arg)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Executes this operation on this Flyway instance.
+     *
+     * @param flyway        The Flyway instance.
+     * @param operation     The operation to execute.
+     * @param sqlStatements The current list of all pending migrations.
+     */
+    private static void executeOperation(final FlywayWithDryRun flyway, final String operation, final Iterable<SqlStatement> sqlStatements) {
+        if ("clean".equals(operation)) {
+            flyway.clean();
+        } else if ("baseline".equals(operation)) {
+            flyway.baseline();
+        } else if ("migrate".equals(operation)) {
+            flyway.migrate();
+        } else if ("dryRunMigrate".equals(operation)) {
+            flyway.dryRunMigrate();
+
+            final StringBuilder stringBuilder = new StringBuilder("BEGIN;\n");
+            for (final SqlStatement sqlStatement : sqlStatements) {
+                stringBuilder.append(sqlStatement.getSql())
+                             .append(";\n");
+            }
+            stringBuilder.append("COMMIT;");
+            LOG.info("\n" + stringBuilder.toString());
+        } else if ("validate".equals(operation)) {
+            flyway.validate();
+        } else if ("info".equals(operation)) {
+            LOG.info("\n" + MigrationInfoDumper.dumpToAsciiTable(flyway.info().all()));
+        } else if ("repair".equals(operation)) {
+            flyway.repair();
+        } else {
+            LOG.error("Invalid operation: " + operation);
+            printUsage();
+            System.exit(1);
+        }
+    }
+
+    /**
+     * Checks the desired log level.
+     *
+     * @param args The command-line arguments.
+     * @return The desired log level.
+     */
+    private static Level getLogLevel(final String[] args) {
+        for (final String arg : args) {
+            if ("-X".equals(arg)) {
+                return Level.DEBUG;
+            }
+            if ("-q".equals(arg)) {
+                return Level.WARN;
+            }
+        }
+        return Level.INFO;
+    }
+
+    /**
+     * Initializes the properties with the default configuration for the command-line tool.
+     *
+     * @param properties The properties object to initialize.
+     */
+    private static void initializeDefaults(final Properties properties) {
+        properties.put("flyway.locations", "filesystem:" + new File(getInstallationDir(), "sql").getAbsolutePath());
+        properties.put(PROPERTY_JAR_DIRS, new File(getInstallationDir(), "jars").getAbsolutePath());
+    }
+
+    /**
+     * Filters there properties to remove the Flyway Commandline-specific ones.
+     *
+     * @param properties The properties to filter.
+     */
+    private static void filterProperties(final Properties properties) {
+        properties.remove(PROPERTY_JAR_DIRS);
+        properties.remove("flyway.configFile");
+        properties.remove("flyway.configFileEncoding");
+    }
+
+    /**
+     * Prints the version number on the console.
+     *
+     * @throws IOException when the version could not be read.
+     */
+    private static void printVersion() throws IOException {
+        final String version = new ClassPathResource("org/flywaydb/core/internal/version.txt", VersionPrinter.class.getClassLoader()).loadAsString("UTF-8");
+        LOG.info("Flyway " + version + " for Kill Bill");
+
+        LOG.debug("Java " + System.getProperty("java.version") + " (" + System.getProperty("java.vendor") + ")");
+        LOG.debug(System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch") + "\n");
+    }
+
+    /**
+     * Prints the usage instructions on the console.
+     */
+    private static void printUsage() {
+        LOG.info("Usage");
+        LOG.info("=====");
+        LOG.info("");
+        LOG.info("flyway [options] command");
+        LOG.info("");
+        LOG.info("By default, the configuration will be read from conf/flyway.conf.");
+        LOG.info("Options passed from the command-line override the configuration.");
+        LOG.info("");
+        LOG.info("Commands");
+        LOG.info("--------");
+        LOG.info("migrate        : Migrates the database");
+        LOG.info("dryRunMigrate  : Migrates the database (dry-run)");
+        LOG.info("clean          : Drops all objects in the configured schemas");
+        LOG.info("info           : Prints the information about applied, current and pending migrations");
+        LOG.info("validate       : Validates the applied migrations against the ones on the classpath");
+        LOG.info("baseline       : Baselines an existing database at the baselineVersion");
+        LOG.info("repair         : Repairs the metadata table");
+        LOG.info("");
+        LOG.info("Options (Format: -key=value)");
+        LOG.info("-------");
+        LOG.info("driver                       : Fully qualified classname of the jdbc driver");
+        LOG.info("url                          : Jdbc url to use to connect to the database");
+        LOG.info("user                         : User to use to connect to the database");
+        LOG.info("password                     : Password to use to connect to the database");
+        LOG.info("schemas                      : Comma-separated list of the schemas managed by Flyway");
+        LOG.info("table                        : Name of Flyway's metadata table");
+        LOG.info("locations                    : Classpath locations to scan recursively for migrations");
+        LOG.info("resolvers                    : Comma-separated list of custom MigrationResolvers");
+        LOG.info("skipDefaultResolvers         : Skips default resolvers (jdbc, sql and Spring-jdbc)");
+        LOG.info("sqlMigrationPrefix           : File name prefix for sql migrations");
+        LOG.info("repeatableSqlMigrationPrefix : File name prefix for repeatable sql migrations");
+        LOG.info("sqlMigrationSeparator        : File name separator for sql migrations");
+        LOG.info("sqlMigrationSuffix           : File name suffix for sql migrations");
+        LOG.info("encoding                     : Encoding of sql migrations");
+        LOG.info("placeholderReplacement       : Whether placeholders should be replaced");
+        LOG.info("placeholders                 : Placeholders to replace in sql migrations");
+        LOG.info("placeholderPrefix            : Prefix of every placeholder");
+        LOG.info("placeholderSuffix            : Suffix of every placeholder");
+        LOG.info("target                       : Target version up to which Flyway should use migrations");
+        LOG.info("outOfOrder                   : Allows migrations to be run \"out of order\"");
+        LOG.info("callbacks                    : Comma-separated list of FlywayCallback classes");
+        LOG.info("skipDefaultCallbacks         : Skips default callbacks (sql)");
+        LOG.info("validateOnMigrate            : Validate when running migrate");
+        LOG.info("ignoreFutureMigrations       : Allow future migrations when validating");
+        LOG.info("cleanOnValidationError       : Automatically clean on a validation error");
+        LOG.info("cleanDisabled                : Whether to disable clean");
+        LOG.info("baselineVersion              : Version to tag schema with when executing baseline");
+        LOG.info("baselineDescription          : Description to tag schema with when executing baseline");
+        LOG.info("baselineOnMigrate            : Baseline on migrate against uninitialized non-empty schema");
+        LOG.info("configFile                   : Config file to use (default: conf/flyway.properties)");
+        LOG.info("configFileEncoding           : Encoding of the config file (default: UTF-8)");
+        LOG.info("jarDirs                      : Dirs for Jdbc drivers & Java migrations (default: jars)");
+        LOG.info("");
+        LOG.info("Add -X to print debug output");
+        LOG.info("Add -q to suppress all output, except for errors and warnings");
+        LOG.info("Add -v to print the Flyway version and exit");
+        LOG.info("");
+        LOG.info("Example");
+        LOG.info("-------");
+        LOG.info("flyway -user=myuser -password=s3cr3t -url=jdbc:h2:mem -placeholders.abc=def migrate");
+        LOG.info("");
+        LOG.info("More info at https://flywaydb.org/documentation/commandline");
+    }
+
+    /**
+     * Loads all the driver jars contained in the drivers folder. (For Jdbc drivers)
+     *
+     * @throws IOException When the jars could not be loaded.
+     */
+    private static void loadJdbcDrivers() throws IOException {
+        final File driversDir = new File(getInstallationDir(), "drivers");
+        final File[] files = driversDir.listFiles(new FilenameFilter() {
+            public boolean accept(final File dir, final String name) {
+                return name.endsWith(".jar");
+            }
+        });
+
+        // see javadoc of listFiles(): null if given path is not a real directory
+        if (files == null) {
+            return;
+        }
+
+        for (final File file : files) {
+            addJarOrDirectoryToClasspath(file.getPath());
+        }
+    }
+
+    /**
+     * Loads all the jars contained in the jars folder. (For Java Migrations)
+     * This will also indirectly load custom driver jars.
+     *
+     * @param properties The configured properties.
+     * @throws IOException When the jars could not be loaded.
+     */
+    private static void loadJavaMigrationsFromJarDirs(final Properties properties) throws IOException {
+        String jarDirs = properties.getProperty(PROPERTY_JAR_DIRS);
+        if (!StringUtils.hasLength(jarDirs)) {
+            return;
+        }
+
+        jarDirs = jarDirs.replace(File.pathSeparator, ",");
+        final String[] dirs = StringUtils.tokenizeToStringArray(jarDirs, ",");
+
+        for (final String dirName : dirs) {
+            final File dir = new File(dirName);
+            final File[] files = dir.listFiles(new FilenameFilter() {
+                public boolean accept(final File dir, final String name) {
+                    return name.endsWith(".jar");
+                }
+            });
+
+            // see javadoc of listFiles(): null if given path is not a real directory
+            if (files == null) {
+                continue;
+            }
+
+            for (final File file : files) {
+                addJarOrDirectoryToClasspath(file.getPath());
+            }
+        }
+    }
+
+    /**
+     * Adds a jar or a directory with this name to the classpath.
+     *
+     * @param name The name of the jar or directory to add.
+     * @throws IOException when the jar or directory could not be found.
+     */
+    private static void addJarOrDirectoryToClasspath(final String name) throws IOException {
+        LOG.debug("Adding location to classpath: " + name);
+
+        try {
+            final URL url = new File(name).toURI().toURL();
+            final URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
+            final Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
+            method.setAccessible(true);
+            method.invoke(sysloader, url);
+        } catch (final Exception e) {
+            throw new FlywayException("Unable to load " + name, e);
+        }
+    }
+
+    /**
+     * Loads the configuration from the various possible locations.
+     *
+     * @param properties The properties object to load to configuration into.
+     * @param args       The command-line arguments passed in.
+     */
+    private static void loadConfiguration(final Properties properties, final String[] args) {
+        final String encoding = determineConfigurationFileEncoding(args);
+
+        loadConfigurationFile(properties, getInstallationDir() + "/conf/flyway.conf", encoding, false);
+        loadConfigurationFile(properties, System.getProperty("user.home") + "/flyway.conf", encoding, false);
+        loadConfigurationFile(properties, "flyway.conf", encoding, false);
+
+        final String configFile = determineConfigurationFileArgument(args);
+        if (configFile != null) {
+            loadConfigurationFile(properties, configFile, encoding, true);
+        }
+    }
+
+    /**
+     * Loads the configuration from the configuration file. If a configuration file is specified using the -configfile
+     * argument it will be used, otherwise the default config file (conf/flyway.properties) will be loaded.
+     *
+     * @param properties    The properties object to load to configuration into.
+     * @param file          The configuration file to load.
+     * @param encoding      The encoding of the configuration file.
+     * @param failIfMissing Whether to fail if the file is missing.
+     * @return Whether the file was loaded successfully.
+     * @throws FlywayException when the configuration file could not be loaded.
+     */
+    private static boolean loadConfigurationFile(final Properties properties, final String file, final String encoding, final boolean failIfMissing) throws FlywayException {
+        final File configFile = new File(file);
+        final String errorMessage = "Unable to load config file: " + configFile.getAbsolutePath();
+
+        if (!configFile.isFile() || !configFile.canRead()) {
+            if (!failIfMissing) {
+                LOG.debug(errorMessage);
+                return false;
+            }
+            throw new FlywayException(errorMessage);
+        }
+
+        LOG.debug("Loading config file: " + configFile.getAbsolutePath());
+        try {
+            final String contents = FileCopyUtils.copyToString(new InputStreamReader(new FileInputStream(configFile), encoding));
+            properties.load(new StringReader(contents.replace("\\", "\\\\")));
+            return true;
+        } catch (final IOException e) {
+            throw new FlywayException(errorMessage, e);
+        }
+    }
+
+    /**
+     * Dumps the configuration to the console when debug output is activated.
+     *
+     * @param properties The configured properties.
+     */
+    private static void dumpConfiguration(final Properties properties) {
+        LOG.debug("Using configuration:");
+        for (final Map.Entry<Object, Object> entry : properties.entrySet()) {
+            String value = entry.getValue().toString();
+            value = "flyway.password".equals(entry.getKey()) ? StringUtils.trimOrPad("", value.length(), '*') : value;
+            LOG.debug(entry.getKey() + " -> " + value);
+        }
+    }
+
+    /**
+     * Determines the file to use for loading the configuration.
+     *
+     * @param args The command-line arguments passed in.
+     * @return The path of the configuration file on disk.
+     */
+    private static String determineConfigurationFileArgument(final String[] args) {
+        for (final String arg : args) {
+            if (isPropertyArgument(arg) && "configFile".equals(getArgumentProperty(arg))) {
+                return getArgumentValue(arg);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * @return The installation directory of the Flyway Command-line tool.
+     */
+    @SuppressWarnings("ConstantConditions")
+    private static String getInstallationDir() {
+        final String path = ClassUtils.getLocationOnDisk(Migrator.class);
+        return new File(path).getParentFile().getParentFile().getAbsolutePath();
+    }
+
+    /**
+     * Determines the encoding to use for loading the configuration.
+     *
+     * @param args The command-line arguments passed in.
+     * @return The encoding. (default: UTF-8)
+     */
+    private static String determineConfigurationFileEncoding(final String[] args) {
+        for (final String arg : args) {
+            if (isPropertyArgument(arg) && "configFileEncoding".equals(getArgumentProperty(arg))) {
+                return getArgumentValue(arg);
+            }
+        }
+
+        return "UTF-8";
+    }
+
+    /**
+     * Overrides the configuration from the config file with the properties passed in directly from the command-line.
+     *
+     * @param properties The properties to override.
+     * @param args       The command-line arguments that were passed in.
+     */
+    private static void overrideConfiguration(final Properties properties, final String[] args) {
+        for (final String arg : args) {
+            if (isPropertyArgument(arg)) {
+                properties.put("flyway." + getArgumentProperty(arg), getArgumentValue(arg));
+            }
+        }
+    }
+
+    /**
+     * Checks whether this command-line argument tries to set a property.
+     *
+     * @param arg The command-line argument to check.
+     * @return {@code true} if it does, {@code false} if not.
+     */
+    private static boolean isPropertyArgument(final String arg) {
+        return arg.startsWith("-") && arg.contains("=");
+    }
+
+    /**
+     * Retrieves the property this command-line argument tries to assign.
+     *
+     * @param arg The command-line argument to check, typically in the form -key=value.
+     * @return The property.
+     */
+    private static String getArgumentProperty(final String arg) {
+        final int index = arg.indexOf("=");
+
+        return arg.substring(1, index);
+    }
+
+    /**
+     * Retrieves the value this command-line argument tries to assign.
+     *
+     * @param arg The command-line argument to check, typically in the form -key=value.
+     * @return The value or an empty string if no value is assigned.
+     */
+    private static String getArgumentValue(final String arg) {
+        final int index = arg.indexOf("=");
+
+        if ((index < 0) || (index == arg.length())) {
+            return "";
+        }
+
+        return arg.substring(index + 1);
+    }
+
+    /**
+     * Determine the operations Flyway should execute.
+     *
+     * @param args The command-line arguments passed in.
+     * @return The operations. An empty list if none.
+     */
+    private static List<String> determineOperations(final String[] args) {
+        final List<String> operations = new ArrayList<String>();
+
+        for (final String arg : args) {
+            if (!arg.startsWith("-")) {
+                operations.add(arg);
+            }
+        }
+
+        return operations;
+    }
+}