killbill-aplcache

history: enhance a few existing tests to verify history tables This

3/29/2017 8:07:24 AM

Details

diff --git a/account/src/test/java/org/killbill/billing/account/dao/TestAccountDao.java b/account/src/test/java/org/killbill/billing/account/dao/TestAccountDao.java
index 237b8f5..e8ea8f0 100644
--- a/account/src/test/java/org/killbill/billing/account/dao/TestAccountDao.java
+++ b/account/src/test/java/org/killbill/billing/account/dao/TestAccountDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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
@@ -26,6 +26,7 @@ import org.joda.time.DateTimeZone;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.account.AccountTestSuiteWithEmbeddedDB;
+import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountApiException;
 import org.killbill.billing.account.api.AccountData;
 import org.killbill.billing.account.api.AccountEmail;
@@ -42,6 +43,7 @@ import org.killbill.billing.util.audit.AuditLog;
 import org.killbill.billing.util.audit.ChangeType;
 import org.killbill.billing.util.audit.DefaultAccountAuditLogs;
 import org.killbill.billing.util.customfield.dao.CustomFieldModelDao;
+import org.killbill.billing.util.dao.EntityHistoryModelDao;
 import org.killbill.billing.util.dao.TableName;
 import org.killbill.billing.util.entity.Pagination;
 import org.killbill.billing.util.tag.DescriptiveTag;
@@ -211,6 +213,18 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
     public void testUpdate() throws Exception {
         final AccountModelDao account = createTestAccount();
         accountDao.create(account, internalCallContext);
+        final AccountModelDao createdAccount = accountDao.getAccountByKey(account.getExternalKey(), internalCallContext);
+
+        final List<EntityHistoryModelDao<AccountModelDao, Account>> history1 = getAccountHistory(createdAccount.getRecordId());
+        Assert.assertEquals(history1.size(), 1);
+        Assert.assertEquals(history1.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history1.get(0).getEntity().getAccountRecordId(), createdAccount.getRecordId());
+        Assert.assertEquals(history1.get(0).getEntity().getTenantRecordId(), createdAccount.getTenantRecordId());
+        Assert.assertEquals(history1.get(0).getEntity().getExternalKey(), createdAccount.getExternalKey());
+        Assert.assertEquals(history1.get(0).getEntity().getMigrated(), createdAccount.getMigrated());
+        Assert.assertEquals(history1.get(0).getEntity().getIsNotifiedForInvoices(), createdAccount.getIsNotifiedForInvoices());
+        Assert.assertEquals(history1.get(0).getEntity().getTimeZone(), createdAccount.getTimeZone());
+        Assert.assertEquals(history1.get(0).getEntity().getLocale(), createdAccount.getLocale());
 
         final AccountData accountData = new MockAccountBuilder(new DefaultAccount(account)).migrated(false)
                                                                                            .isNotifiedForInvoices(false)
@@ -222,6 +236,40 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
 
         final AccountModelDao retrievedAccount = accountDao.getAccountByKey(account.getExternalKey(), internalCallContext);
         checkAccountsEqual(retrievedAccount, updatedAccount);
+
+        final List<EntityHistoryModelDao<AccountModelDao, Account>> history2 = getAccountHistory(createdAccount.getRecordId());
+        Assert.assertEquals(history2.size(), 2);
+        Assert.assertEquals(history2.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history2.get(1).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history2.get(1).getEntity().getAccountRecordId(), retrievedAccount.getRecordId());
+        Assert.assertEquals(history2.get(1).getEntity().getTenantRecordId(), retrievedAccount.getTenantRecordId());
+        Assert.assertEquals(history2.get(1).getEntity().getExternalKey(), retrievedAccount.getExternalKey());
+        Assert.assertEquals(history2.get(1).getEntity().getMigrated(), retrievedAccount.getMigrated());
+        Assert.assertEquals(history2.get(1).getEntity().getIsNotifiedForInvoices(), retrievedAccount.getIsNotifiedForInvoices());
+        Assert.assertEquals(history2.get(1).getEntity().getTimeZone(), retrievedAccount.getTimeZone());
+        Assert.assertEquals(history2.get(1).getEntity().getLocale(), retrievedAccount.getLocale());
+
+        final AccountData accountData2 = new MockAccountBuilder(new DefaultAccount(updatedAccount)).isNotifiedForInvoices(true)
+                                                                                                   .locale("en_US")
+                                                                                                   .build();
+        final AccountModelDao updatedAccount2 = new AccountModelDao(account.getId(), accountData2);
+        accountDao.update(updatedAccount2, internalCallContext);
+
+        final AccountModelDao retrievedAccount2 = accountDao.getAccountByKey(account.getExternalKey(), internalCallContext);
+        checkAccountsEqual(retrievedAccount2, updatedAccount2);
+
+        final List<EntityHistoryModelDao<AccountModelDao, Account>> history3 = getAccountHistory(createdAccount.getRecordId());
+        Assert.assertEquals(history3.size(), 3);
+        Assert.assertEquals(history3.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history3.get(1).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history3.get(2).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history3.get(2).getEntity().getAccountRecordId(), retrievedAccount2.getRecordId());
+        Assert.assertEquals(history3.get(2).getEntity().getTenantRecordId(), retrievedAccount2.getTenantRecordId());
+        Assert.assertEquals(history3.get(2).getEntity().getExternalKey(), retrievedAccount2.getExternalKey());
+        Assert.assertEquals(history3.get(2).getEntity().getMigrated(), retrievedAccount2.getMigrated());
+        Assert.assertEquals(history3.get(2).getEntity().getIsNotifiedForInvoices(), retrievedAccount2.getIsNotifiedForInvoices());
+        Assert.assertEquals(history3.get(2).getEntity().getTimeZone(), retrievedAccount2.getTimeZone());
+        Assert.assertEquals(history3.get(2).getEntity().getLocale(), retrievedAccount2.getLocale());
     }
 
     @Test(groups = "slow", description = "Test Account DAO: payment method update")
@@ -351,4 +399,10 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         Assert.assertEquals(auditLogsForAccountEmail2.size(), 1);
         Assert.assertEquals(auditLogsForAccountEmail2.get(0).getChangeType(), ChangeType.INSERT);
     }
+
+    private List<EntityHistoryModelDao<AccountModelDao, Account>> getAccountHistory(final Long accountRecordId) {
+        // See https://github.com/killbill/killbill/issues/335
+        final AccountSqlDao accountSqlDao = dbi.onDemand(AccountSqlDao.class);
+        return accountSqlDao.getHistoryForTargetRecordId(accountRecordId, internalCallContext);
+    }
 }
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorWithDB.java b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorWithDB.java
index 79f4b34..3912a51 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorWithDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorWithDB.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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,12 +17,18 @@
 
 package org.killbill.billing.payment.core;
 
+import java.util.List;
 import java.util.UUID;
 
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
 import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentMethod;
 import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.dao.PaymentMethodModelDao;
+import org.killbill.billing.payment.dao.PaymentMethodSqlDao;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.dao.EntityHistoryModelDao;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -55,4 +61,43 @@ public class TestPaymentMethodProcessorWithDB extends PaymentTestSuiteWithEmbedd
         final Account accountById = accountApi.getAccountById(account.getId(), internalCallContext);
         Assert.assertEquals(accountById.getPaymentMethodId(), newPaymentMethod);
     }
+
+    @Test(groups = "slow")
+    public void testDeletePaymentMethod() throws Exception {
+        final Account account = testHelper.createTestAccount("foo@bar.com", true);
+
+        final UUID paymentMethodId = paymentMethodProcessor.createOrGetExternalPaymentMethod("pmExternalKey", account, PLUGIN_PROPERTIES, callContext, internalCallContext);
+        final PaymentMethodModelDao paymentMethodModelDao = paymentDao.getPaymentMethod(paymentMethodId, internalCallContext);
+
+        final List<EntityHistoryModelDao<PaymentMethodModelDao, PaymentMethod>> history1 = getPaymentMethodHistory(paymentMethodModelDao.getRecordId());
+        Assert.assertEquals(history1.size(), 1);
+        Assert.assertEquals(history1.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history1.get(0).getEntity().getAccountRecordId(), paymentMethodModelDao.getAccountRecordId());
+        Assert.assertEquals(history1.get(0).getEntity().getTenantRecordId(), paymentMethodModelDao.getTenantRecordId());
+        Assert.assertEquals(history1.get(0).getEntity().getExternalKey(), paymentMethodModelDao.getExternalKey());
+        Assert.assertTrue(history1.get(0).getEntity().isActive());
+
+        paymentMethodProcessor.deletedPaymentMethod(account, paymentMethodId, true, true, ImmutableList.<PluginProperty>of(), callContext, internalCallContext);
+
+        final List<EntityHistoryModelDao<PaymentMethodModelDao, PaymentMethod>> history2 = getPaymentMethodHistory(paymentMethodModelDao.getRecordId());
+        Assert.assertEquals(history2.size(), 2);
+        Assert.assertEquals(history2.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history2.get(0).getEntity().getAccountRecordId(), paymentMethodModelDao.getAccountRecordId());
+        Assert.assertEquals(history2.get(0).getEntity().getTenantRecordId(), paymentMethodModelDao.getTenantRecordId());
+        Assert.assertEquals(history2.get(0).getEntity().getExternalKey(), paymentMethodModelDao.getExternalKey());
+        Assert.assertTrue(history2.get(0).getEntity().isActive());
+        // Note: it looks like we don't consider this as a DELETE, probably because we can un-delete such payment methods?
+        Assert.assertEquals(history2.get(1).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history2.get(1).getEntity().getAccountRecordId(), paymentMethodModelDao.getAccountRecordId());
+        Assert.assertEquals(history2.get(1).getEntity().getTenantRecordId(), paymentMethodModelDao.getTenantRecordId());
+        Assert.assertEquals(history2.get(1).getEntity().getExternalKey(), paymentMethodModelDao.getExternalKey());
+        // Note: upon deletion, the recorded state is the same as before the delete
+        Assert.assertTrue(history2.get(1).getEntity().isActive());
+    }
+
+    private List<EntityHistoryModelDao<PaymentMethodModelDao, PaymentMethod>> getPaymentMethodHistory(final Long paymentMethodRecordId) {
+        // See https://github.com/killbill/killbill/issues/335
+        final PaymentMethodSqlDao paymentMethodSqlDao = dbi.onDemand(PaymentMethodSqlDao.class);
+        return paymentMethodSqlDao.getHistoryForTargetRecordId(paymentMethodRecordId, internalCallContext);
+    }
 }
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 a195593..a23c0a0 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
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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
@@ -28,13 +28,14 @@ import org.joda.time.DateTime;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
+import org.killbill.billing.payment.api.Payment;
 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.dao.PluginPropertySerializer.PluginPropertySerializerException;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.dao.EntityHistoryModelDao;
 import org.killbill.billing.util.entity.Pagination;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -95,7 +96,6 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
 
     @Test(groups = "slow")
     public void testPaymentAndTransactions() {
-
         final UUID paymentMethodId = UUID.randomUUID();
         final UUID accountId = UUID.randomUUID();
         final String externalKey = "hhhhooo";
@@ -117,6 +117,17 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
         assertEquals(savedPayment.getPaymentMethodId(), paymentModelDao.getPaymentMethodId());
         assertNull(savedPayment.getStateName());
 
+        final List<EntityHistoryModelDao<PaymentModelDao, Payment>> history1 = getPaymentHistory(savedPayment.getRecordId());
+        Assert.assertEquals(history1.size(), 1);
+        Assert.assertEquals(history1.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history1.get(0).getEntity().getAccountRecordId(), savedPayment.getAccountRecordId());
+        Assert.assertEquals(history1.get(0).getEntity().getTenantRecordId(), savedPayment.getTenantRecordId());
+        Assert.assertEquals(history1.get(0).getEntity().getExternalKey(), savedPayment.getExternalKey());
+        Assert.assertEquals(history1.get(0).getEntity().getStateName(), savedPayment.getStateName());
+        Assert.assertEquals(history1.get(0).getEntity().getLastSuccessStateName(), savedPayment.getLastSuccessStateName());
+        Assert.assertNull(history1.get(0).getEntity().getStateName());
+        Assert.assertNull(history1.get(0).getEntity().getLastSuccessStateName());
+
         final PaymentModelDao savedPayment2 = paymentDao.getPayment(savedPayment.getId(), internalCallContext);
         assertEquals(savedPayment2.getId(), paymentModelDao.getId());
         assertEquals(savedPayment2.getAccountId(), paymentModelDao.getAccountId());
@@ -162,6 +173,20 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
         assertEquals(savedTransactionModelDao2.getAmount().compareTo(BigDecimal.TEN), 0);
         assertEquals(savedTransactionModelDao2.getCurrency(), Currency.AED);
 
+        final List<EntityHistoryModelDao<PaymentModelDao, Payment>> history2 = getPaymentHistory(savedPayment.getRecordId());
+        Assert.assertEquals(history2.size(), 2);
+        Assert.assertEquals(history2.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history2.get(0).getEntity().getAccountRecordId(), savedPayment.getAccountRecordId());
+        Assert.assertEquals(history2.get(0).getEntity().getTenantRecordId(), savedPayment.getTenantRecordId());
+        Assert.assertEquals(history2.get(0).getEntity().getExternalKey(), savedPayment.getExternalKey());
+        Assert.assertEquals(history2.get(1).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history2.get(1).getEntity().getAccountRecordId(), savedPayment.getAccountRecordId());
+        Assert.assertEquals(history2.get(1).getEntity().getTenantRecordId(), savedPayment.getTenantRecordId());
+        Assert.assertEquals(history2.get(1).getEntity().getExternalKey(), savedPayment.getExternalKey());
+        Assert.assertTrue(history2.get(1).getEntity().getUpdatedDate().compareTo(history2.get(0).getEntity().getUpdatedDate()) >= 0);
+        Assert.assertNull(history2.get(1).getEntity().getStateName());
+        Assert.assertNull(history2.get(1).getEntity().getLastSuccessStateName());
+
         final List<PaymentTransactionModelDao> transactions = paymentDao.getTransactionsForPayment(savedPayment.getId(), internalCallContext);
         assertEquals(transactions.size(), 2);
 
@@ -176,6 +201,25 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
         assertEquals(savedPayment4.getStateName(), "AUTH_ABORTED");
         assertEquals(savedPayment4.getLastSuccessStateName(), "AUTH_SUCCESS");
 
+        final List<EntityHistoryModelDao<PaymentModelDao, Payment>> history3 = getPaymentHistory(savedPayment.getRecordId());
+        Assert.assertEquals(history3.size(), 3);
+        Assert.assertEquals(history3.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history3.get(0).getEntity().getAccountRecordId(), savedPayment.getAccountRecordId());
+        Assert.assertEquals(history3.get(0).getEntity().getTenantRecordId(), savedPayment.getTenantRecordId());
+        Assert.assertEquals(history3.get(0).getEntity().getExternalKey(), savedPayment.getExternalKey());
+        Assert.assertEquals(history3.get(1).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history3.get(1).getEntity().getAccountRecordId(), savedPayment.getAccountRecordId());
+        Assert.assertEquals(history3.get(1).getEntity().getTenantRecordId(), savedPayment.getTenantRecordId());
+        Assert.assertEquals(history3.get(1).getEntity().getExternalKey(), savedPayment.getExternalKey());
+        Assert.assertTrue(history3.get(1).getEntity().getUpdatedDate().compareTo(history3.get(0).getEntity().getUpdatedDate()) >= 0);
+        Assert.assertEquals(history3.get(2).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history3.get(2).getEntity().getAccountRecordId(), savedPayment.getAccountRecordId());
+        Assert.assertEquals(history3.get(2).getEntity().getTenantRecordId(), savedPayment.getTenantRecordId());
+        Assert.assertEquals(history3.get(2).getEntity().getExternalKey(), savedPayment.getExternalKey());
+        Assert.assertTrue(history3.get(2).getEntity().getUpdatedDate().compareTo(history3.get(2).getEntity().getUpdatedDate()) >= 0);
+        Assert.assertEquals(history3.get(2).getEntity().getStateName(), savedPayment4.getStateName());
+        Assert.assertEquals(history3.get(2).getEntity().getLastSuccessStateName(), savedPayment4.getLastSuccessStateName());
+
         final PaymentTransactionModelDao savedTransactionModelDao4 = paymentDao.getPaymentTransaction(savedTransactionModelDao2.getId(), internalCallContext);
         assertEquals(savedTransactionModelDao4.getTransactionExternalKey(), transactionExternalKey2);
         assertEquals(savedTransactionModelDao4.getPaymentId(), paymentModelDao.getId());
@@ -550,5 +594,11 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
             }
         }));
     }
+
+    private List<EntityHistoryModelDao<PaymentModelDao, Payment>> getPaymentHistory(final Long paymentRecordId) {
+        // See https://github.com/killbill/killbill/issues/335
+        final PaymentSqlDao paymentSqlDao = dbi.onDemand(PaymentSqlDao.class);
+        return paymentSqlDao.getHistoryForTargetRecordId(paymentRecordId, internalCallContext);
+    }
 }
 
diff --git a/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDaoMapper.java b/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDaoMapper.java
new file mode 100644
index 0000000..5ff331d
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDaoMapper.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 Groupon, Inc
+ * Copyright 2017 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.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+import org.killbill.billing.util.entity.dao.EntityModelDaoBase;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class EntityHistoryModelDaoMapper<M extends EntityModelDao<E>, E extends Entity> extends MapperBase implements ResultSetMapper<EntityHistoryModelDao<M, E>> {
+
+    private final ResultSetMapper<M> entityMapper;
+
+    public EntityHistoryModelDaoMapper(final ResultSetMapper<M> entityMapper) {
+        this.entityMapper = entityMapper;
+    }
+
+    @Override
+    public EntityHistoryModelDao<M, E> map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+        final UUID id = getUUID(r, "id");
+        final long targetRecordId = r.getLong("target_record_id");
+        final String changeType = r.getString("change_type");
+        final DateTime createdDate = getDateTime(r, "created_date");
+
+        final M entityModelDao = entityMapper.map(index, r, ctx);
+
+        // Hack -- remove the id as it is the history id, not the entity id
+        ((EntityModelDaoBase) entityModelDao).setId(null);
+        // Hack -- similarly, populate the right record_id
+        ((EntityModelDaoBase) entityModelDao).setRecordId(targetRecordId);
+        // Hack -- account is special
+        if (entityModelDao.getAccountRecordId() == null) {
+            ((EntityModelDaoBase) entityModelDao).setAccountRecordId(targetRecordId);
+        }
+
+        return new EntityHistoryModelDao(id, entityModelDao, targetRecordId, ChangeType.valueOf(changeType), createdDate);
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDaoMapperFactory.java b/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDaoMapperFactory.java
new file mode 100644
index 0000000..38cc98d
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDaoMapperFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Groupon, Inc
+ * Copyright 2017 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.dao;
+
+import org.killbill.commons.jdbi.mapper.LowerToCamelBeanMapper;
+import org.skife.jdbi.v2.ResultSetMapperFactory;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class EntityHistoryModelDaoMapperFactory implements ResultSetMapperFactory {
+
+    private final Class<?> sqlObjectType;
+    private final Class<?> modelClazz;
+
+    public EntityHistoryModelDaoMapperFactory(final Class modelClazz, final Class<?> sqlObjectType) {
+        this.sqlObjectType = sqlObjectType;
+        this.modelClazz = modelClazz;
+    }
+
+    @Override
+    public boolean accepts(final Class type, final StatementContext ctx) {
+        return type.isAssignableFrom(EntityHistoryModelDao.class) && sqlObjectType.equals(ctx.getSqlObjectType());
+    }
+
+    @Override
+    public ResultSetMapper mapperFor(final Class type, final StatementContext ctx) {
+        return new EntityHistoryModelDaoMapper(new LowerToCamelBeanMapper(modelClazz));
+    }
+}
+
diff --git a/util/src/main/java/org/killbill/billing/util/dao/HistorySqlDao.java b/util/src/main/java/org/killbill/billing/util/dao/HistorySqlDao.java
index c829280..1a748c1 100644
--- a/util/src/main/java/org/killbill/billing/util/dao/HistorySqlDao.java
+++ b/util/src/main/java/org/killbill/billing/util/dao/HistorySqlDao.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2011 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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,15 +18,22 @@
 
 package org.killbill.billing.util.dao;
 
-import org.skife.jdbi.v2.sqlobject.BindBean;
-import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import java.util.List;
 
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.util.entity.Entity;
 import org.killbill.billing.util.entity.dao.EntityModelDao;
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 
 public interface HistorySqlDao<M extends EntityModelDao<E>, E extends Entity> {
 
+    @SqlQuery
+    public List<EntityHistoryModelDao<M, E>> getHistoryForTargetRecordId(@Bind("targetRecordId") final long targetRecordId,
+                                                                         @BindBean InternalCallContext context);
+
     @SqlUpdate
     public void addHistoryFromTransaction(@EntityHistoryBinder EntityHistoryModelDao<M, E> history,
                                           @BindBean InternalCallContext context);
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoStringTemplate.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoStringTemplate.java
index 12d9517..ce2fd8d 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoStringTemplate.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoStringTemplate.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2012 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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:
  *
@@ -28,6 +30,8 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.regex.Matcher;
 
+import org.killbill.billing.util.dao.EntityHistoryModelDaoMapperFactory;
+import org.killbill.billing.util.entity.Entity;
 import org.killbill.commons.jdbi.mapper.LowerToCamelBeanMapperFactory;
 import org.skife.jdbi.v2.Query;
 import org.skife.jdbi.v2.SQLStatement;
@@ -38,8 +42,6 @@ import org.skife.jdbi.v2.sqlobject.stringtemplate.StringTemplate3StatementLocato
 import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
 import org.skife.jdbi.v2.tweak.StatementLocator;
 
-import org.killbill.billing.util.entity.Entity;
-
 @SqlStatementCustomizingAnnotation(EntitySqlDaoStringTemplate.EntitySqlDaoLocatorFactory.class)
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.TYPE})
@@ -114,6 +116,7 @@ public @interface EntitySqlDaoStringTemplate {
                                             final Class modelClazz = (Class) modelType;
                                             if (Entity.class.isAssignableFrom(modelClazz)) {
                                                 query.registerMapper(new LowerToCamelBeanMapperFactory(modelClazz));
+                                                query.registerMapper(new EntityHistoryModelDaoMapperFactory(modelClazz, sqlObjectType));
                                             }
                                         }
                                     }
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 a79883d..2d96592 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
@@ -336,6 +336,18 @@ auditTableValues() ::= <<
 <if(tenantRecordIdField(""))>, <tenantRecordIdValue()><endif>
 >>
 
+getHistoryForTargetRecordId() ::= <<
+select
+  <idField()>
+, <historyTableFields("t.")>
+<accountRecordIdFieldWithComma("t.")>
+<tenantRecordIdFieldWithComma("t.")>
+from <historyTableName()> t
+where <targetRecordIdField("t.")> = :targetRecordId
+<AND_CHECK_TENANT("t.")>
+order by <recordIdField("t.")> ASC
+;
+>>
 
 addHistoryFromTransaction() ::= <<
 insert into <historyTableName()> (