Repository: killbill/killbill
Issue: Sub-millisecond precision in catalog date can lead to duplicate invoice items

If the catalog effective date is set to `2020-09-16T10:34:25.348646Z` for instance, the DDL must support sub-millisecond precision (by default, it doesn't).

Changed files:
invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceItemDao.java
invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemCatalogBase.java

diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemCatalogBase.java b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemCatalogBase.java
index 398f49bb61..65c0cd69cb 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemCatalogBase.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemCatalogBase.java
@@ -25,6 +25,7 @@ import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
+import org.joda.time.Seconds;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoiceItemType;
@@ -127,7 +128,11 @@ public class InvoiceItemCatalogBase extends InvoiceItemBase implements InvoiceIt
             return false;
         }
         final InvoiceItemCatalogBase that = (InvoiceItemCatalogBase) o;
-        if (catalogEffectiveDate != null && that.catalogEffectiveDate != null && catalogEffectiveDate.compareTo(that.catalogEffectiveDate) != 0) {
+        if (catalogEffectiveDate != null &&
+            that.catalogEffectiveDate != null &&
+            catalogEffectiveDate.compareTo(that.catalogEffectiveDate) != 0 &&
+            // https://github.com/killbill/killbill/issues/1370
+            Math.abs(Seconds.secondsBetween(catalogEffectiveDate, that.catalogEffectiveDate).getSeconds()) != 0) {
             return false;
         } else if (catalogEffectiveDate == null || that.catalogEffectiveDate == null) {
             /* At least one such item has a 'null' catalogEffectiveDate, reflecting a prior 0.22 release item
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 43e974cdd4..8c1bbd0698 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
@@ -22,6 +22,7 @@ import java.math.BigDecimal;
 import java.util.List;
 import java.util.UUID;
 
+import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 import org.killbill.billing.invoice.model.TaxInvoiceItem;
 import org.testng.Assert;
@@ -243,6 +244,29 @@ public class TestInvoiceItemDao extends InvoiceTestSuiteWithEmbeddedDB {
         createAndVerifyExternalCharge(new BigDecimal("10.2"), Currency.MGA);
     }
 
+    @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/1370")
+    public void testFixedPriceWithSubMillisecondCatalogEffectiveDateInvoiceSqlDao() throws EntityPersistenceException {
+        final UUID invoiceId = UUID.randomUUID();
+        final UUID accountId = account.getId();
+        final LocalDate startDate = new LocalDate(2012, 4, 1);
+
+        final InvoiceItem fixedPriceInvoiceItem = new FixedPriceInvoiceItem(invoiceId,
+                                                                            accountId,
+                                                                            UUID.randomUUID(),
+                                                                            UUID.randomUUID(),
+                                                                            "test product",
+                                                                            "test plan",
+                                                                            "test phase",
+                                                                            new DateTime("2020-09-16T10:34:25.348646Z"),
+                                                                            startDate,
+                                                                            TEN,
+                                                                            Currency.USD);
+        invoiceUtil.createInvoiceItem(fixedPriceInvoiceItem, context);
+
+        final InvoiceItemModelDao savedItem = invoiceUtil.getInvoiceItemById(fixedPriceInvoiceItem.getId(), context);
+        assertSameInvoiceItem(fixedPriceInvoiceItem, savedItem);
+    }
+
     private void createAndVerifyExternalCharge(final BigDecimal amount, final Currency currency) throws EntityPersistenceException {
         final InvoiceItem externalChargeInvoiceItem = new ExternalChargeInvoiceItem(UUID.randomUUID(), account.getId(), UUID.randomUUID(),
                                                                                     UUID.randomUUID().toString(), new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), amount, currency, null);