killbill-uncached

pagination: various bugfixes This fixes: * Pagination

12/10/2015 10:53:08 AM

Changes

Details

diff --git a/account/src/test/java/org/killbill/billing/account/dao/MockAccountDao.java b/account/src/test/java/org/killbill/billing/account/dao/MockAccountDao.java
index e9715dd..a06aa3f 100644
--- a/account/src/test/java/org/killbill/billing/account/dao/MockAccountDao.java
+++ b/account/src/test/java/org/killbill/billing/account/dao/MockAccountDao.java
@@ -107,7 +107,9 @@ public class MockAccountDao extends MockEntityDaoBase<AccountModelDao, Account, 
     @Override
     public Pagination<AccountModelDao> searchAccounts(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
         final List<AccountModelDao> results = new LinkedList<AccountModelDao>();
+        int maxNbRecords = 0;
         for (final AccountModelDao account : getAll(context)) {
+            maxNbRecords++;
             if ((account.getName() != null && account.getName().contains(searchKey)) ||
                 (account.getEmail() != null && account.getEmail().contains(searchKey)) ||
                 (account.getExternalKey() != null && account.getExternalKey().contains(searchKey)) ||
@@ -116,7 +118,7 @@ public class MockAccountDao extends MockEntityDaoBase<AccountModelDao, Account, 
             }
         }
 
-        return DefaultPagination.<AccountModelDao>build(offset, limit, results);
+        return DefaultPagination.<AccountModelDao>build(offset, limit, maxNbRecords, results);
     }
 
     @Override
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 d6a2133..6c1c27e 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
@@ -182,7 +182,9 @@ public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice, 
     @Override
     public Pagination<InvoiceModelDao> searchInvoices(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
         final List<InvoiceModelDao> results = new LinkedList<InvoiceModelDao>();
+        int maxNbRecords = 0;
         for (final InvoiceModelDao invoice : getAll(context)) {
+            maxNbRecords++;
             if (invoice.getId().toString().equals(searchKey) ||
                 invoice.getAccountId().toString().equals(searchKey) ||
                 invoice.getInvoiceNumber().toString().equals(searchKey) ||
@@ -191,7 +193,7 @@ public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice, 
             }
         }
 
-        return DefaultPagination.<InvoiceModelDao>build(offset, limit, results);
+        return DefaultPagination.<InvoiceModelDao>build(offset, limit, maxNbRecords, results);
     }
 
     @Override
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 f09a73f..0429c46 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
@@ -56,6 +56,7 @@ import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.callcontext.TenantContext;
 import org.killbill.billing.util.config.PaymentConfig;
+import org.killbill.billing.util.entity.DefaultPagination;
 import org.killbill.billing.util.entity.Pagination;
 import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.EntityPaginationBuilder;
 import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
@@ -70,6 +71,7 @@ import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
 import com.google.inject.Inject;
 
 import static org.killbill.billing.payment.dispatcher.PaymentPluginDispatcher.dispatchWithExceptionHandling;
@@ -236,7 +238,8 @@ public class PaymentMethodProcessor extends ProcessorBase {
     }
 
     public Pagination<PaymentMethod> getPaymentMethods(final Long offset, final Long limit, final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) {
-        return getEntityPaginationFromPlugins(getAvailablePlugins(),
+        return getEntityPaginationFromPlugins(true,
+                                              getAvailablePlugins(),
                                               offset,
                                               limit,
                                               new EntityPaginationBuilder<PaymentMethod, PaymentApiException>() {
@@ -279,69 +282,75 @@ public class PaymentMethodProcessor extends ProcessorBase {
     }
 
     public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) {
-        return getEntityPaginationFromPlugins(getAvailablePlugins(),
-                                              offset,
-                                              limit,
-                                              new EntityPaginationBuilder<PaymentMethod, PaymentApiException>() {
-                                                  @Override
-                                                  public Pagination<PaymentMethod> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
-                                                      return searchPaymentMethods(searchKey, offset, limit, pluginName, withPluginInfo, properties, tenantContext, internalTenantContext);
-                                                  }
-                                              }
-                                             );
-    }
-
-    public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final String pluginName,
-                                                          final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
         if (withPluginInfo) {
-            final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
-
-            return getEntityPagination(limit,
-                                       new SourcePaginationBuilder<PaymentMethodPlugin, PaymentApiException>() {
-                                           @Override
-                                           public Pagination<PaymentMethodPlugin> build() throws PaymentApiException {
-                                               try {
-                                                   return pluginApi.searchPaymentMethods(searchKey, offset, limit, properties, tenantContext);
-                                               } catch (final PaymentPluginApiException e) {
-                                                   throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_PAYMENT_METHODS, pluginName, searchKey);
+            return getEntityPaginationFromPlugins(false,
+                                                  getAvailablePlugins(),
+                                                  offset,
+                                                  limit,
+                                                  new EntityPaginationBuilder<PaymentMethod, PaymentApiException>() {
+                                                      @Override
+                                                      public Pagination<PaymentMethod> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
+                                                          return searchPaymentMethods(searchKey, offset, limit, pluginName, withPluginInfo, properties, tenantContext, internalTenantContext);
+                                                      }
+                                                  }
+                                                 );
+        } else {
+            try {
+                return getEntityPagination(limit,
+                                           new SourcePaginationBuilder<PaymentMethodModelDao, PaymentApiException>() {
+                                               @Override
+                                               public Pagination<PaymentMethodModelDao> build() {
+                                                   return paymentDao.searchPaymentMethods(searchKey, offset, limit, internalTenantContext);
                                                }
-                                           }
-                                       },
-                                       new Function<PaymentMethodPlugin, PaymentMethod>() {
-                                           @Override
-                                           public PaymentMethod apply(final PaymentMethodPlugin paymentMethodPlugin) {
-                                               if (paymentMethodPlugin.getKbPaymentMethodId() == null) {
-                                                   // Garbage from the plugin?
-                                                   log.debug("Plugin {} returned a payment method without a kbPaymentMethodId for searchKey {}", pluginName, searchKey);
-                                                   return null;
+                                           },
+                                           new Function<PaymentMethodModelDao, PaymentMethod>() {
+                                               @Override
+                                               public PaymentMethod apply(final PaymentMethodModelDao paymentMethodModelDao) {
+                                                   return new DefaultPaymentMethod(paymentMethodModelDao, null);
                                                }
+                                           }
+                                          );
+            } catch (final PaymentApiException e) {
+                log.warn("Unable to search through payment methods", e);
+                return new DefaultPagination<PaymentMethod>(offset, limit, null, null, Iterators.<PaymentMethod>emptyIterator());
+            }
+        }
+    }
 
-                                               final PaymentMethodModelDao paymentMethodModelDao = paymentDao.getPaymentMethodIncludedDeleted(paymentMethodPlugin.getKbPaymentMethodId(), internalTenantContext);
-                                               if (paymentMethodModelDao == null) {
-                                                   log.warn("Unable to find payment method id " + paymentMethodPlugin.getKbPaymentMethodId() + " present in plugin " + pluginName);
-                                                   return null;
-                                               }
+    public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final String pluginName, final boolean withPluginInfo,
+                                                          final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+        final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
 
-                                               return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
+        return getEntityPagination(limit,
+                                   new SourcePaginationBuilder<PaymentMethodPlugin, PaymentApiException>() {
+                                       @Override
+                                       public Pagination<PaymentMethodPlugin> build() throws PaymentApiException {
+                                           try {
+                                               return pluginApi.searchPaymentMethods(searchKey, offset, limit, properties, tenantContext);
+                                           } catch (final PaymentPluginApiException e) {
+                                               throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_PAYMENT_METHODS, pluginName, searchKey);
                                            }
                                        }
-                                      );
-        } else {
-            return getEntityPagination(limit,
-                                       new SourcePaginationBuilder<PaymentMethodModelDao, PaymentApiException>() {
-                                           @Override
-                                           public Pagination<PaymentMethodModelDao> build() {
-                                               return paymentDao.searchPaymentMethods(searchKey, offset, limit, internalTenantContext);
+                                   },
+                                   new Function<PaymentMethodPlugin, PaymentMethod>() {
+                                       @Override
+                                       public PaymentMethod apply(final PaymentMethodPlugin paymentMethodPlugin) {
+                                           if (paymentMethodPlugin.getKbPaymentMethodId() == null) {
+                                               // Garbage from the plugin?
+                                               log.debug("Plugin {} returned a payment method without a kbPaymentMethodId for searchKey {}", pluginName, searchKey);
+                                               return null;
                                            }
-                                       },
-                                       new Function<PaymentMethodModelDao, PaymentMethod>() {
-                                           @Override
-                                           public PaymentMethod apply(final PaymentMethodModelDao paymentMethodModelDao) {
-                                               return new DefaultPaymentMethod(paymentMethodModelDao, null);
+
+                                           final PaymentMethodModelDao paymentMethodModelDao = paymentDao.getPaymentMethodIncludedDeleted(paymentMethodPlugin.getKbPaymentMethodId(), internalTenantContext);
+                                           if (paymentMethodModelDao == null) {
+                                               log.warn("Unable to find payment method id " + paymentMethodPlugin.getKbPaymentMethodId() + " present in plugin " + pluginName);
+                                               return null;
                                            }
+
+                                           return new DefaultPaymentMethod(paymentMethodModelDao, withPluginInfo ? paymentMethodPlugin : null);
                                        }
-                                      );
-        }
+                                   }
+                                  );
     }
 
     public PaymentMethod getExternalPaymentMethod(final UUID accountId, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext context) throws PaymentApiException {
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
index 7b39cf1..7aa74e5 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
@@ -60,6 +60,7 @@ import org.killbill.billing.tag.TagInternalApi;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.DefaultPagination;
 import org.killbill.billing.util.entity.Pagination;
 import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.EntityPaginationBuilder;
 import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
@@ -72,6 +73,7 @@ import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
 
@@ -206,7 +208,8 @@ public class PaymentProcessor extends ProcessorBase {
 
     public Pagination<Payment> getPayments(final Long offset, final Long limit, final boolean withPluginInfo, final Iterable<PluginProperty> properties,
                                            final TenantContext tenantContext, final InternalTenantContext internalTenantContext) {
-        return getEntityPaginationFromPlugins(getAvailablePlugins(),
+        return getEntityPaginationFromPlugins(true,
+                                              getAvailablePlugins(),
                                               offset,
                                               limit,
                                               new EntityPaginationBuilder<Payment, PaymentApiException>() {
@@ -240,74 +243,80 @@ public class PaymentProcessor extends ProcessorBase {
     }
 
     public Pagination<Payment> searchPayments(final String searchKey, final Long offset, final Long limit, final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) {
-        return getEntityPaginationFromPlugins(getAvailablePlugins(),
-                                              offset,
-                                              limit,
-                                              new EntityPaginationBuilder<Payment, PaymentApiException>() {
-                                                  @Override
-                                                  public Pagination<Payment> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
-                                                      return searchPayments(searchKey, offset, limit, pluginName, withPluginInfo, properties, tenantContext, internalTenantContext);
-                                                  }
-                                              }
-                                             );
-    }
-
-    public Pagination<Payment> searchPayments(final String searchKey, final Long offset, final Long limit, final String pluginName, final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
         if (withPluginInfo) {
-            final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
-
-            return getEntityPagination(limit,
-                                       new SourcePaginationBuilder<PaymentTransactionInfoPlugin, PaymentApiException>() {
-                                           @Override
-                                           public Pagination<PaymentTransactionInfoPlugin> build() throws PaymentApiException {
-                                               try {
-                                                   return pluginApi.searchPayments(searchKey, offset, limit, properties, tenantContext);
-                                               } catch (final PaymentPluginApiException e) {
-                                                   throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_PAYMENTS, pluginName, searchKey);
+            return getEntityPaginationFromPlugins(false,
+                                                  getAvailablePlugins(),
+                                                  offset,
+                                                  limit,
+                                                  new EntityPaginationBuilder<Payment, PaymentApiException>() {
+                                                      @Override
+                                                      public Pagination<Payment> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
+                                                          return searchPayments(searchKey, offset, limit, pluginName, withPluginInfo, properties, tenantContext, internalTenantContext);
+                                                      }
+                                                  }
+                                                 );
+        } else {
+            try {
+                return getEntityPagination(limit,
+                                           new SourcePaginationBuilder<PaymentModelDao, PaymentApiException>() {
+                                               @Override
+                                               public Pagination<PaymentModelDao> build() {
+                                                   return paymentDao.searchPayments(searchKey, offset, limit, internalTenantContext);
+                                               }
+                                           },
+                                           new Function<PaymentModelDao, Payment>() {
+                                               @Override
+                                               public Payment apply(final PaymentModelDao paymentModelDao) {
+                                                   return toPayment(paymentModelDao.getId(), null, internalTenantContext);
                                                }
                                            }
+                                          );
+            } catch (final PaymentApiException e) {
+                log.warn("Unable to search through payments", e);
+                return new DefaultPagination<Payment>(offset, limit, null, null, Iterators.<Payment>emptyIterator());
+            }
+        }
+    }
 
-                                       },
-                                       new Function<PaymentTransactionInfoPlugin, Payment>() {
-                                           final List<PaymentTransactionInfoPlugin> cachedPaymentTransactions = new LinkedList<PaymentTransactionInfoPlugin>();
-
-                                           @Override
-                                           public Payment apply(final PaymentTransactionInfoPlugin pluginTransaction) {
-                                               if (pluginTransaction.getKbPaymentId() == null) {
-                                                   // Garbage from the plugin?
-                                                   log.debug("Plugin {} returned a payment without a kbPaymentId for searchKey {}", pluginName, searchKey);
-                                                   return null;
-                                               }
+    public Pagination<Payment> searchPayments(final String searchKey, final Long offset, final Long limit, final String pluginName, final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+        final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
 
-                                               if (cachedPaymentTransactions.isEmpty() ||
-                                                   (cachedPaymentTransactions.get(0).getKbPaymentId().equals(pluginTransaction.getKbPaymentId()))) {
-                                                   cachedPaymentTransactions.add(pluginTransaction);
-                                                   return null;
-                                               } else {
-                                                   final Payment result = toPayment(pluginTransaction.getKbPaymentId(), ImmutableList.<PaymentTransactionInfoPlugin>copyOf(cachedPaymentTransactions), internalTenantContext);
-                                                   cachedPaymentTransactions.clear();
-                                                   cachedPaymentTransactions.add(pluginTransaction);
-                                                   return result;
-                                               }
+        return getEntityPagination(limit,
+                                   new SourcePaginationBuilder<PaymentTransactionInfoPlugin, PaymentApiException>() {
+                                       @Override
+                                       public Pagination<PaymentTransactionInfoPlugin> build() throws PaymentApiException {
+                                           try {
+                                               return pluginApi.searchPayments(searchKey, offset, limit, properties, tenantContext);
+                                           } catch (final PaymentPluginApiException e) {
+                                               throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_PAYMENTS, pluginName, searchKey);
                                            }
                                        }
-                                      );
-        } else {
-            return getEntityPagination(limit,
-                                       new SourcePaginationBuilder<PaymentModelDao, PaymentApiException>() {
-                                           @Override
-                                           public Pagination<PaymentModelDao> build() {
-                                               return paymentDao.searchPayments(searchKey, offset, limit, internalTenantContext);
+
+                                   },
+                                   new Function<PaymentTransactionInfoPlugin, Payment>() {
+                                       final List<PaymentTransactionInfoPlugin> cachedPaymentTransactions = new LinkedList<PaymentTransactionInfoPlugin>();
+
+                                       @Override
+                                       public Payment apply(final PaymentTransactionInfoPlugin pluginTransaction) {
+                                           if (pluginTransaction.getKbPaymentId() == null) {
+                                               // Garbage from the plugin?
+                                               log.debug("Plugin {} returned a payment without a kbPaymentId for searchKey {}", pluginName, searchKey);
+                                               return null;
                                            }
-                                       },
-                                       new Function<PaymentModelDao, Payment>() {
-                                           @Override
-                                           public Payment apply(final PaymentModelDao paymentModelDao) {
-                                               return toPayment(paymentModelDao.getId(), null, internalTenantContext);
+
+                                           if (cachedPaymentTransactions.isEmpty() ||
+                                               (cachedPaymentTransactions.get(0).getKbPaymentId().equals(pluginTransaction.getKbPaymentId()))) {
+                                               cachedPaymentTransactions.add(pluginTransaction);
+                                               return null;
+                                           } else {
+                                               final Payment result = toPayment(pluginTransaction.getKbPaymentId(), withPluginInfo ? ImmutableList.<PaymentTransactionInfoPlugin>copyOf(cachedPaymentTransactions) : ImmutableList.<PaymentTransactionInfoPlugin>of(), internalTenantContext);
+                                               cachedPaymentTransactions.clear();
+                                               cachedPaymentTransactions.add(pluginTransaction);
+                                               return result;
                                            }
                                        }
-                                      );
-        }
+                                   }
+                                  );
     }
 
     private Payment performOperation(final boolean isApiPayment, @Nullable final UUID attemptId,
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 210d6da..aeccb57 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
@@ -54,7 +54,7 @@ getByExternalKey() ::= <<
 select <allTableFields()>
 from <tableName()>
 where external_key = :externalKey
-and is_active = true
+<andCheckSoftDeletionWithComma()>
 <AND_CHECK_TENANT()>
 ;
 >>
@@ -80,7 +80,7 @@ select
 <allTableFields()>
 from <tableName()>
 where account_id = :accountId
-and is_active = true
+<andCheckSoftDeletionWithComma()>
 <AND_CHECK_TENANT()>
 ;
 >>
@@ -106,7 +106,7 @@ select
 <allTableFields("t.")>
 from <tableName()> t
 where t.plugin_name = :pluginName
-and t.is_active = true
+<andCheckSoftDeletionWithComma("t.")>
 <AND_CHECK_TENANT("t.")>
 order by t.record_id
 limit :rowCount offset :offset
@@ -118,7 +118,7 @@ select
   count(1) as count
 from <tableName()> t
 where t.plugin_name = :pluginName
-and t.is_active = true
+<andCheckSoftDeletionWithComma("t.")>
 <AND_CHECK_TENANT("t.")>
 ;
 >>
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 52e64ac..2fc2626 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
@@ -26,15 +26,10 @@ import java.util.UUID;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeoutException;
 
-import org.killbill.automaton.OperationException;
-import org.killbill.billing.payment.logging.SpyLogger;
-import org.killbill.billing.payment.provider.ExternalPaymentProviderPlugin;
-import org.killbill.commons.request.Request;
-import org.killbill.commons.request.RequestData;
-
 import javax.annotation.Nullable;
 
 import org.joda.time.LocalDate;
+import org.killbill.automaton.OperationException;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.catalog.api.Currency;
@@ -48,8 +43,11 @@ import org.killbill.billing.payment.core.sm.OperationCallbackBase;
 import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
 import org.killbill.billing.payment.dao.PaymentSqlDao;
 import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
+import org.killbill.billing.payment.logging.SpyLogger;
 import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.provider.ExternalPaymentProviderPlugin;
 import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+import org.killbill.billing.util.entity.Pagination;
 import org.killbill.bus.api.PersistentBus.EventBusException;
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
@@ -108,7 +106,22 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
         }
     }
 
-        @Test(groups = "slow")
+    @Test(groups = "slow")
+    public void testAddRemovePaymentMethod() throws Exception {
+        final Long baseNbRecords = paymentApi.getPaymentMethods(0L, 1000L, false, ImmutableList.<PluginProperty>of(), callContext).getMaxNbRecords();
+        Assert.assertEquals(baseNbRecords, (Long) 1L);
+
+        final Account account = testHelper.createTestAccount(UUID.randomUUID().toString(), true);
+        final UUID paymentMethodId = account.getPaymentMethodId();
+
+        checkPaymentMethodPagination(paymentMethodId, baseNbRecords + 1, false);
+
+        paymentApi.deletePaymentMethod(account, paymentMethodId, true, ImmutableList.<PluginProperty>of(), callContext);
+
+        checkPaymentMethodPagination(paymentMethodId, baseNbRecords, true);
+    }
+
+    @Test(groups = "slow")
     public void testCreateSuccessPurchase() throws PaymentApiException {
 
         final BigDecimal requestedAmount = BigDecimal.TEN;
@@ -1031,4 +1044,47 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
         result.add(new PluginProperty(InvoicePaymentControlPluginApi.PROP_IPCD_INVOICE_ID, invoice.getId().toString(), false));
         return result;
     }
+
+    // Search by a key supported by the search in MockPaymentProviderPlugin
+    private void checkPaymentMethodPagination(final UUID paymentMethodId, final Long maxNbRecords, final boolean deleted) throws PaymentApiException {
+        final Pagination<PaymentMethod> foundPaymentMethods = paymentApi.searchPaymentMethods(paymentMethodId.toString(), 0L, maxNbRecords + 1, false, ImmutableList.<PluginProperty>of(), callContext);
+        Assert.assertEquals(foundPaymentMethods.iterator().hasNext(), !deleted);
+        Assert.assertEquals(foundPaymentMethods.getMaxNbRecords(), maxNbRecords);
+        Assert.assertEquals(foundPaymentMethods.getTotalNbRecords(), (Long) (!deleted ? 1L : 0L));
+
+        final Pagination<PaymentMethod> foundPaymentMethodsWithPluginInfo = paymentApi.searchPaymentMethods(paymentMethodId.toString(), 0L, maxNbRecords + 1, true, ImmutableList.<PluginProperty>of(), callContext);
+        Assert.assertEquals(foundPaymentMethodsWithPluginInfo.iterator().hasNext(), !deleted);
+        Assert.assertEquals(foundPaymentMethodsWithPluginInfo.getMaxNbRecords(), maxNbRecords);
+        Assert.assertEquals(foundPaymentMethodsWithPluginInfo.getTotalNbRecords(), (Long) (!deleted ? 1L : 0L));
+
+        final Pagination<PaymentMethod> foundPaymentMethods2 = paymentApi.searchPaymentMethods(paymentMethodId.toString(), 0L, maxNbRecords + 1, MockPaymentProviderPlugin.PLUGIN_NAME, false, ImmutableList.<PluginProperty>of(), callContext);
+        Assert.assertEquals(foundPaymentMethods2.iterator().hasNext(), !deleted);
+        Assert.assertEquals(foundPaymentMethods2.getMaxNbRecords(), maxNbRecords);
+        Assert.assertEquals(foundPaymentMethods2.getTotalNbRecords(), (Long) (!deleted ? 1L : 0L));
+
+        final Pagination<PaymentMethod> foundPaymentMethods2WithPluginInfo = paymentApi.searchPaymentMethods(paymentMethodId.toString(), 0L, maxNbRecords + 1, MockPaymentProviderPlugin.PLUGIN_NAME, true, ImmutableList.<PluginProperty>of(), callContext);
+        Assert.assertEquals(foundPaymentMethods2WithPluginInfo.iterator().hasNext(), !deleted);
+        Assert.assertEquals(foundPaymentMethods2WithPluginInfo.getMaxNbRecords(), maxNbRecords);
+        Assert.assertEquals(foundPaymentMethods2WithPluginInfo.getTotalNbRecords(), (Long) (!deleted ? 1L : 0L));
+
+        final Pagination<PaymentMethod> gotPaymentMethods = paymentApi.getPaymentMethods(0L, maxNbRecords + 1L, false, ImmutableList.<PluginProperty>of(), callContext);
+        Assert.assertEquals(gotPaymentMethods.iterator().hasNext(), maxNbRecords > 0);
+        Assert.assertEquals(gotPaymentMethods.getMaxNbRecords(), maxNbRecords);
+        Assert.assertEquals(gotPaymentMethods.getTotalNbRecords(), maxNbRecords);
+
+        final Pagination<PaymentMethod> gotPaymentMethodsWithPluginInfo = paymentApi.getPaymentMethods(0L, maxNbRecords + 1L, true, ImmutableList.<PluginProperty>of(), callContext);
+        Assert.assertEquals(gotPaymentMethodsWithPluginInfo.iterator().hasNext(), maxNbRecords > 0);
+        Assert.assertEquals(gotPaymentMethodsWithPluginInfo.getMaxNbRecords(), maxNbRecords);
+        Assert.assertEquals(gotPaymentMethodsWithPluginInfo.getTotalNbRecords(), maxNbRecords);
+
+        final Pagination<PaymentMethod> gotPaymentMethods2 = paymentApi.getPaymentMethods(0L, maxNbRecords + 1L, MockPaymentProviderPlugin.PLUGIN_NAME, false, ImmutableList.<PluginProperty>of(), callContext);
+        Assert.assertEquals(gotPaymentMethods2.iterator().hasNext(), maxNbRecords > 0);
+        Assert.assertEquals(gotPaymentMethods2.getMaxNbRecords(), maxNbRecords);
+        Assert.assertEquals(gotPaymentMethods2.getTotalNbRecords(), maxNbRecords);
+
+        final Pagination<PaymentMethod> gotPaymentMethods2WithPluginInfo = paymentApi.getPaymentMethods(0L, maxNbRecords + 1L, MockPaymentProviderPlugin.PLUGIN_NAME, true, ImmutableList.<PluginProperty>of(), callContext);
+        Assert.assertEquals(gotPaymentMethods2WithPluginInfo.iterator().hasNext(), maxNbRecords > 0);
+        Assert.assertEquals(gotPaymentMethods2WithPluginInfo.getMaxNbRecords(), maxNbRecords);
+        Assert.assertEquals(gotPaymentMethods2WithPluginInfo.getTotalNbRecords(), maxNbRecords);
+    }
 }
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
index 537c603..8788e73 100644
--- a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
@@ -305,7 +305,7 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
                 return (input.getKbPaymentMethodId().toString().equals(searchKey));
             }
         }));
-        return DefaultPagination.<PaymentMethodPlugin>build(offset, limit, results);
+        return DefaultPagination.<PaymentMethodPlugin>build(offset, limit, paymentMethods.size(), results);
     }
 
     @Override
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
index 6aaed33..af1970f 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
@@ -126,7 +126,9 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
     @Override
     public Pagination<SubscriptionBundleModelDao> searchSubscriptionBundles(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
         final List<SubscriptionBundleModelDao> results = new LinkedList<SubscriptionBundleModelDao>();
+        int maxNbRecords = 0;
         for (final SubscriptionBundleModelDao bundleModelDao : getAll(context)) {
+            maxNbRecords++;
             if (bundleModelDao.getId().toString().equals(searchKey) ||
                 bundleModelDao.getExternalKey().equals(searchKey) ||
                 bundleModelDao.getAccountId().toString().equals(searchKey)) {
@@ -134,7 +136,7 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
             }
         }
 
-        return DefaultPagination.<SubscriptionBundleModelDao>build(offset, limit, results);
+        return DefaultPagination.<SubscriptionBundleModelDao>build(offset, limit, maxNbRecords, results);
     }
 
     @Override
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 6b0c800..a417357 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
@@ -42,7 +42,7 @@ public class DefaultPaginationHelper {
         public abstract Pagination<E> build(final Long offset, final Long limit, final String pluginName) throws T;
     }
 
-    public static <E extends Entity, T extends BillingExceptionBase> Pagination<E> getEntityPaginationFromPlugins(final Iterable<String> plugins, final Long offset, final Long limit, final EntityPaginationBuilder<E, T> entityPaginationBuilder) {
+    public static <E extends Entity, T extends BillingExceptionBase> Pagination<E> getEntityPaginationFromPlugins(final boolean maxStatsCrossPlugins, final Iterable<String> plugins, final Long offset, final Long limit, final EntityPaginationBuilder<E, T> entityPaginationBuilder) {
         // Note that we cannot easily do streaming here, since we would have to rely on the statistics
         // returned by the Pagination objects from the plugins and we probably don't want to do that (if
         // one plugin gets it wrong, it may starve the others).
@@ -67,7 +67,12 @@ public class DefaultPaginationHelper {
                 // Make sure not to start at 0 for subsequent plugins if previous ones didn't yield any result
                 firstSearch = allResults.isEmpty();
                 totalNbRecords += pages.getTotalNbRecords();
-                maxNbRecords += pages.getMaxNbRecords();
+                if (!maxStatsCrossPlugins) {
+                    maxNbRecords += pages.getMaxNbRecords();
+                } else {
+                    // getPayments and getPaymentMethods return MaxNbRecords across all plugins -- make sure we don't double count
+                    maxNbRecords = Math.max(maxNbRecords, pages.getMaxNbRecords());
+                }
             } catch (final BillingExceptionBase 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/entity/dao/DefaultPaginationSqlDaoHelper.java b/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
index 1f9a9fd..8157d11 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2014 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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:
  *
@@ -61,6 +63,9 @@ public class DefaultPaginationSqlDaoHelper {
 
     public abstract static class PaginationIteratorBuilder<M extends EntityModelDao<E>, E extends Entity, S extends EntitySqlDao<M, E>> {
 
+        // Determine the totalNbRecords:
+        // - For search queries, return the "SearchCount", i.e. the number of records matching the search query.
+        // - For get calls, return the total number of records (totalNbRecords == maxNbRecords)
         public abstract Long getCount(final S sqlDao, final InternalTenantContext context);
 
         public abstract Iterator<M> build(final S sqlDao, final Long limit, final InternalTenantContext context);
diff --git a/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java b/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java
index 3abdf37..81401d6 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java
@@ -33,9 +33,13 @@ public class DefaultPagination<T> implements Pagination<T> {
     private final Long maxNbRecords;
     private final Iterator<T> delegateIterator;
 
-    // Builder when the streaming API can't be used (should only be used for tests)
+    // Builders when the streaming API can't be used (should only be used for tests)
     // Notes: elements should be the entire records set (regardless of filtering) otherwise maxNbRecords won't be accurate
     public static <T> Pagination<T> build(final Long offset, final Long limit, final Collection<T> elements) {
+        return build(offset, limit, elements.size(), elements);
+    }
+
+    public static <T> Pagination<T> build(final Long offset, final Long limit, final Integer maxNbRecords, final Collection<T> elements) {
         final List<T> allResults = ImmutableList.<T>copyOf(elements);
 
         final List<T> results;
@@ -46,7 +50,7 @@ public class DefaultPagination<T> implements Pagination<T> {
         } else {
             results = allResults.subList(offset.intValue(), offset.intValue() + limit.intValue());
         }
-        return new DefaultPagination<T>(offset, limit, (long) results.size(), (long) allResults.size(), results.iterator());
+        return new DefaultPagination<T>(offset, limit, (long) results.size(), (long) maxNbRecords, results.iterator());
     }
 
     // Constructor for DAO -> API bridge
diff --git a/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
index fe9ac05..13022fe 100644
--- a/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
+++ b/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
@@ -268,6 +268,7 @@ select
 <allTableFields("t.")>
 from <tableName()> t
 where (<searchQuery("t.")>)
+<andCheckSoftDeletionWithComma("t.")>
 <AND_CHECK_TENANT("t.")>
 order by <recordIdField("t.")> ASC
 limit :rowCount offset :offset
@@ -279,6 +280,7 @@ select
   count(1) as count
 from <tableName()> t
 where (<searchQuery("t.")>)
+<andCheckSoftDeletionWithComma("t.")>
 <AND_CHECK_TENANT("t.")>
 ;
 >>
diff --git a/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
index 1b82e3c..00c7bbe 100644
--- a/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
+++ b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
@@ -64,6 +64,7 @@ userAndSystemTagDefinitions() ::= <<
   , name
   , description
   from tag_definitions
+  where is_active
   union
   select
     \'00000000-0000-0000-0000-000000000001\' id
@@ -114,6 +115,7 @@ select
 from <tableName()> t
 join (<userAndSystemTagDefinitions()>) td on td.id = t.tag_definition_id
 where (<searchQuery(tagAlias="t.", tagDefinitionAlias="td.")>)
+<andCheckSoftDeletionWithComma("t.")>
 <AND_CHECK_TENANT("t.")>
 order by <recordIdField("t.")> ASC
 limit :rowCount offset :offset
@@ -126,6 +128,7 @@ select
 from <tableName()> t
 join (<userAndSystemTagDefinitions()>) td on td.id = t.tag_definition_id
 where (<searchQuery(tagAlias="t.", tagDefinitionAlias="td.")>)
+<andCheckSoftDeletionWithComma("t.")>
 <AND_CHECK_TENANT("t.")>
 ;
 >>
diff --git a/util/src/test/java/org/killbill/billing/mock/api/MockAccountUserApi.java b/util/src/test/java/org/killbill/billing/mock/api/MockAccountUserApi.java
index d365b82..18e47eb 100644
--- a/util/src/test/java/org/killbill/billing/mock/api/MockAccountUserApi.java
+++ b/util/src/test/java/org/killbill/billing/mock/api/MockAccountUserApi.java
@@ -118,7 +118,7 @@ public class MockAccountUserApi implements AccountUserApi {
                 results.add(account);
             }
         }
-        return DefaultPagination.<Account>build(offset, limit, results);
+        return DefaultPagination.<Account>build(offset, limit, accounts.size(), results);
     }
 
     @Override
diff --git a/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java b/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java
index 5a60177..92f6715 100644
--- a/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java
+++ b/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2012 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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:
  *
@@ -21,16 +23,16 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
-import org.skife.jdbi.v2.Handle;
-import org.skife.jdbi.v2.tweak.HandleCallback;
-import org.testng.Assert;
-import org.testng.annotations.Test;
-
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
 import org.killbill.billing.util.customfield.CustomField;
 import org.killbill.billing.util.customfield.StringCustomField;
+import org.killbill.billing.util.entity.Pagination;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.testng.Assert;
+import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableList;
 
@@ -52,6 +54,8 @@ public class TestDefaultCustomFieldUserApi extends UtilTestSuiteWithEmbeddedDB {
             }
         });
 
+        checkPagination(0);
+
         final String cfName = UUID.randomUUID().toString().substring(1, 4);
         final String cfValue = UUID.randomUUID().toString().substring(1, 4);
         final CustomField customField = new StringCustomField(cfName, cfValue, ObjectType.ACCOUNT, accountId, callContext.getCreatedDate());
@@ -59,6 +63,8 @@ public class TestDefaultCustomFieldUserApi extends UtilTestSuiteWithEmbeddedDB {
         customFieldUserApi.addCustomFields(ImmutableList.<CustomField>of(customField), callContext);
         assertListenerStatus();
 
+        checkPagination(1);
+
         // Verify the field was saved
         final List<CustomField> customFields = customFieldUserApi.getCustomFieldsForObject(accountId, ObjectType.ACCOUNT, callContext);
         Assert.assertEquals(customFields.size(), 1);
@@ -82,6 +88,8 @@ public class TestDefaultCustomFieldUserApi extends UtilTestSuiteWithEmbeddedDB {
         List<CustomField> remainingCustomFields = customFieldUserApi.getCustomFieldsForObject(accountId, ObjectType.ACCOUNT, callContext);
         Assert.assertEquals(remainingCustomFields.size(), 0);
 
+        checkPagination(0);
+
         // Add again the custom field
         final CustomField newCustomField = new StringCustomField(cfName, cfValue, ObjectType.ACCOUNT, accountId, callContext.getCreatedDate());
 
@@ -90,10 +98,25 @@ public class TestDefaultCustomFieldUserApi extends UtilTestSuiteWithEmbeddedDB {
         remainingCustomFields = customFieldUserApi.getCustomFieldsForObject(accountId, ObjectType.ACCOUNT, callContext);
         Assert.assertEquals(remainingCustomFields.size(), 1);
 
+        checkPagination(1);
+
         // Delete again
         customFieldUserApi.removeCustomFields(remainingCustomFields, callContext);
         remainingCustomFields = customFieldUserApi.getCustomFieldsForObject(accountId, ObjectType.ACCOUNT, callContext);
         Assert.assertEquals(remainingCustomFields.size(), 0);
 
+        checkPagination(0);
+    }
+
+    private void checkPagination(final long nbRecords) {
+        final Pagination<CustomField> foundCustomFields = customFieldUserApi.searchCustomFields("ACCOUNT", 0L, nbRecords + 1L, callContext);
+        Assert.assertEquals(foundCustomFields.iterator().hasNext(), nbRecords > 0);
+        Assert.assertEquals(foundCustomFields.getMaxNbRecords(), (Long) nbRecords);
+        Assert.assertEquals(foundCustomFields.getTotalNbRecords(), (Long) nbRecords);
+
+        final Pagination<CustomField> gotCustomFields = customFieldUserApi.getCustomFields(0L, nbRecords + 1L, callContext);
+        Assert.assertEquals(gotCustomFields.iterator().hasNext(), nbRecords > 0);
+        Assert.assertEquals(gotCustomFields.getMaxNbRecords(), (Long) nbRecords);
+        Assert.assertEquals(gotCustomFields.getTotalNbRecords(), (Long) nbRecords);
     }
 }
diff --git a/util/src/test/java/org/killbill/billing/util/tag/api/TestDefaultTagUserApi.java b/util/src/test/java/org/killbill/billing/util/tag/api/TestDefaultTagUserApi.java
new file mode 100644
index 0000000..7042c2b
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/api/TestDefaultTagUserApi.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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.tag.api;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.tag.ControlTagType;
+import org.killbill.billing.util.tag.Tag;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestDefaultTagUserApi extends UtilTestSuiteWithEmbeddedDB {
+
+    @Test(groups = "slow")
+    public void testSaveTagWithAccountRecordId() throws Exception {
+        final UUID accountId = UUID.randomUUID();
+        final Long accountRecordId = 19384012L;
+
+        dbi.withHandle(new HandleCallback<Void>() {
+            @Override
+            public Void withHandle(final Handle handle) throws Exception {
+                // Note: we always create an accounts table, see MysqlTestingHelper
+                handle.execute("insert into accounts (record_id, id, email, name, first_name_length, is_notified_for_invoices, created_date, created_by, updated_date, updated_by) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+                               accountRecordId, accountId.toString(), "yo@t.com", "toto", 4, false, new Date(), "i", new Date(), "j");
+
+                return null;
+            }
+        });
+
+        checkPagination(0);
+
+        eventsListener.pushExpectedEvent(NextEvent.TAG);
+        tagUserApi.addTags(accountId, ObjectType.ACCOUNT, ImmutableList.<UUID>of(ControlTagType.WRITTEN_OFF.getId()), callContext);
+        assertListenerStatus();
+
+        checkPagination(1);
+
+        // Verify the tag was saved
+        final List<Tag> tags = tagUserApi.getTagsForObject(accountId, ObjectType.ACCOUNT, true, callContext);
+        Assert.assertEquals(tags.size(), 1);
+        Assert.assertEquals(tags.get(0).getTagDefinitionId(), ControlTagType.WRITTEN_OFF.getId());
+        Assert.assertEquals(tags.get(0).getObjectId(), accountId);
+        Assert.assertEquals(tags.get(0).getObjectType(), ObjectType.ACCOUNT);
+        // Verify the account_record_id was populated
+        dbi.withHandle(new HandleCallback<Void>() {
+            @Override
+            public Void withHandle(final Handle handle) throws Exception {
+                final List<Map<String, Object>> values = handle.select("select account_record_id from tags where object_id = ?", accountId.toString());
+                Assert.assertEquals(values.size(), 1);
+                Assert.assertEquals(values.get(0).keySet().size(), 1);
+                Assert.assertEquals(Long.valueOf(values.get(0).get("account_record_id").toString()), accountRecordId);
+                return null;
+            }
+        });
+
+        tagUserApi.removeTags(accountId, ObjectType.ACCOUNT, ImmutableList.<UUID>of(ControlTagType.WRITTEN_OFF.getId()), callContext);
+        List<Tag> remainingTags = tagUserApi.getTagsForObject(accountId, ObjectType.ACCOUNT, false, callContext);
+        Assert.assertEquals(remainingTags.size(), 0);
+
+        checkPagination(0);
+
+        // Add again the tag
+        eventsListener.pushExpectedEvent(NextEvent.TAG);
+        tagUserApi.addTags(accountId, ObjectType.ACCOUNT, ImmutableList.<UUID>of(ControlTagType.WRITTEN_OFF.getId()), callContext);
+        remainingTags = tagUserApi.getTagsForObject(accountId, ObjectType.ACCOUNT, false, callContext);
+        Assert.assertEquals(remainingTags.size(), 1);
+
+        checkPagination(1);
+
+        // Delete again
+        tagUserApi.removeTags(accountId, ObjectType.ACCOUNT, ImmutableList.<UUID>of(ControlTagType.WRITTEN_OFF.getId()), callContext);
+        remainingTags = tagUserApi.getTagsForObject(accountId, ObjectType.ACCOUNT, false, callContext);
+        Assert.assertEquals(remainingTags.size(), 0);
+
+        checkPagination(0);
+    }
+
+    private void checkPagination(final long nbRecords) {
+        final Pagination<Tag> foundTags = tagUserApi.searchTags("ACCOUNT", 0L, nbRecords + 1L, callContext);
+        Assert.assertEquals(foundTags.iterator().hasNext(), nbRecords > 0);
+        Assert.assertEquals(foundTags.getMaxNbRecords(), (Long) nbRecords);
+        Assert.assertEquals(foundTags.getTotalNbRecords(), (Long) nbRecords);
+
+        final Pagination<Tag> gotTags = tagUserApi.getTags(0L, nbRecords + 1L, callContext);
+        Assert.assertEquals(gotTags.iterator().hasNext(), nbRecords > 0);
+        Assert.assertEquals(gotTags.getMaxNbRecords(), (Long) nbRecords);
+        Assert.assertEquals(gotTags.getTotalNbRecords(), (Long) nbRecords);
+    }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java b/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java
index 5e15c74..5317d3f 100644
--- a/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java
+++ b/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java
@@ -33,6 +33,7 @@ import org.killbill.billing.util.dao.NonEntityDao;
 import org.killbill.billing.util.export.dao.DatabaseExportDao;
 import org.killbill.billing.util.glue.TestUtilModuleWithEmbeddedDB;
 import org.killbill.billing.util.nodes.dao.NodeInfoDao;
+import org.killbill.billing.util.tag.api.DefaultTagUserApi;
 import org.killbill.billing.util.tag.dao.DefaultTagDao;
 import org.killbill.billing.util.tag.dao.TagDefinitionDao;
 import org.killbill.bus.api.PersistentBus;
@@ -65,6 +66,8 @@ public abstract class UtilTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuite
     @Inject
     protected InternalCallContextFactory internalCallContextFactory;
     @Inject
+    protected DefaultTagUserApi tagUserApi;
+    @Inject
     protected DefaultCustomFieldUserApi customFieldUserApi;
     @Inject
     protected CustomFieldDao customFieldDao;