killbill-memoizeit

Details

diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 1eb38a9..1d459ec 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -28,7 +28,7 @@
       <option name="useParameterizedTypeForCollectionMethods" value="true" />
       <option name="doNotWeakenToJavaLangObject" value="true" />
       <option name="onlyWeakentoInterface" value="true" />
-      <stopClasses>org.killbill.billing.catalog.api.Plan</stopClasses>
+      <stopClasses>org.killbill.billing.catalog.api.Plan,org.killbill.billing.junction.BillingEventSet</stopClasses>
     </inspection_tool>
     <inspection_tool class="UnnecessaryConstantArrayCreationExpression" enabled="true" level="WARNING" enabled_by_default="true" />
     <inspection_tool class="UseOfJDBCDriverClass" enabled="true" level="WARNING" enabled_by_default="true" />
diff --git a/account/src/main/java/org/killbill/billing/account/api/DefaultAccount.java b/account/src/main/java/org/killbill/billing/account/api/DefaultAccount.java
index 5e8e383..d36d976 100644
--- a/account/src/main/java/org/killbill/billing/account/api/DefaultAccount.java
+++ b/account/src/main/java/org/killbill/billing/account/api/DefaultAccount.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -304,6 +304,7 @@ public class DefaultAccount extends EntityBase implements Account {
      * @return merged account data
      */
     @Override
+    @Deprecated // TODO Get rid of this in 0.22
     public Account mergeWithDelegate(final Account currentAccount) {
         final DefaultMutableAccountData accountData = new DefaultMutableAccountData(this);
 
diff --git a/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java b/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
index 44e8b4d..8cf856a 100644
--- a/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
+++ b/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
@@ -101,7 +101,7 @@ public class DefaultAccountInternalApi extends DefaultAccountApiBase implements 
         final AccountModelDao accountToUpdate = new AccountModelDao(currentAccount.getId(), mutableAccountData);
         bcdCacheController.remove(currentAccount.getId());
         bcdCacheController.putIfAbsent(currentAccount.getId(), new Integer(bcd));
-        accountDao.update(accountToUpdate, context);
+        accountDao.update(accountToUpdate, true, context);
     }
 
     @Override
diff --git a/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountUserApi.java b/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountUserApi.java
index be52cb0..c69c1a1 100644
--- a/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountUserApi.java
+++ b/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountUserApi.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -156,30 +156,12 @@ public class DefaultAccountUserApi extends DefaultAccountApiBase implements Acco
 
     @Override
     public void updateAccount(final Account account, final CallContext context) throws AccountApiException {
-
-        // Convert to DefaultAccount to make sure we can safely call validateAccountUpdateInput
-        final DefaultAccount input = new DefaultAccount(account.getId(), account);
-
-        final Account currentAccount = getAccountById(input.getId(), context);
-        if (currentAccount == null) {
-            throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_ID, input.getId());
-        }
-
-        input.validateAccountUpdateInput(currentAccount, true);
-
-        final AccountModelDao updatedAccountModelDao = new AccountModelDao(currentAccount.getId(),  input);
-
-        accountDao.update(updatedAccountModelDao, internalCallContextFactory.createInternalCallContext(updatedAccountModelDao.getId(), context));
+        updateAccount(account.getId(), account, true, context);
     }
 
     @Override
     public void updateAccount(final UUID accountId, final AccountData accountData, final CallContext context) throws AccountApiException {
-        final Account currentAccount = getAccountById(accountId, context);
-        if (currentAccount == null) {
-            throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_ID, accountId);
-        }
-
-        updateAccount(currentAccount, accountData, context);
+        updateAccount(accountId, accountData, false, context);
     }
 
     @Override
@@ -189,18 +171,16 @@ public class DefaultAccountUserApi extends DefaultAccountApiBase implements Acco
             throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_KEY, externalKey);
         }
 
-        updateAccount(currentAccount, accountData, context);
+        updateAccount(currentAccount.getId(), accountData, false, context);
     }
 
-    private void updateAccount(final Account currentAccount, final AccountData accountData, final CallContext context) throws AccountApiException {
-        final Account updatedAccount = new DefaultAccount(currentAccount.getId(), accountData);
-
-        // Set unspecified (null) fields to their current values
-        final Account mergedAccount = updatedAccount.mergeWithDelegate(currentAccount);
-
-        final AccountModelDao updatedAccountModelDao = new AccountModelDao(currentAccount.getId(), mergedAccount);
-
-        accountDao.update(updatedAccountModelDao, internalCallContextFactory.createInternalCallContext(updatedAccountModelDao.getId(), context));
+    private void updateAccount(final UUID accountId, final AccountData account, final boolean treatNullValueAsReset, final CallContext context) throws AccountApiException {
+        final AccountModelDao updatedAccountModelDao = new AccountModelDao(accountId,
+                                                                           null,
+                                                                           null,
+                                                                           account,
+                                                                           false);
+        accountDao.update(updatedAccountModelDao, treatNullValueAsReset, internalCallContextFactory.createInternalCallContext(accountId, context));
     }
 
     @Override
diff --git a/account/src/main/java/org/killbill/billing/account/dao/AccountDao.java b/account/src/main/java/org/killbill/billing/account/dao/AccountDao.java
index 81de578..dfd8722 100644
--- a/account/src/main/java/org/killbill/billing/account/dao/AccountDao.java
+++ b/account/src/main/java/org/killbill/billing/account/dao/AccountDao.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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:
  *
@@ -45,7 +47,7 @@ public interface AccountDao extends EntityDao<AccountModelDao, Account, AccountA
      */
     void updatePaymentMethod(UUID accountId, UUID paymentMethodId, InternalCallContext context) throws AccountApiException;
 
-    void update(AccountModelDao account, InternalCallContext context) throws AccountApiException;
+    void update(AccountModelDao account, boolean treatNullValueAsReset, InternalCallContext context) throws AccountApiException;
 
     void addEmail(AccountEmailModelDao email, InternalCallContext context) throws AccountApiException;
 
@@ -60,5 +62,4 @@ public interface AccountDao extends EntityDao<AccountModelDao, Account, AccountA
     List<AuditLogWithHistory> getAuditLogsWithHistoryForId(UUID accountId, AuditLevel auditLevel, InternalTenantContext context) throws AccountApiException;
 
     List<AuditLogWithHistory> getEmailAuditLogsWithHistoryForId(UUID accountEmailId, AuditLevel auditLevel, InternalTenantContext context) throws AccountApiException;
-
 }
diff --git a/account/src/main/java/org/killbill/billing/account/dao/AccountModelDao.java b/account/src/main/java/org/killbill/billing/account/dao/AccountModelDao.java
index 5154025..6146f66 100644
--- a/account/src/main/java/org/killbill/billing/account/dao/AccountModelDao.java
+++ b/account/src/main/java/org/killbill/billing/account/dao/AccountModelDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -64,15 +64,15 @@ public class AccountModelDao extends EntityModelDaoBase implements TimeZoneAware
 
     public AccountModelDao() { /* For the DAO mapper */ }
 
-    public AccountModelDao(final UUID id, final DateTime createdDate, final DateTime updatedDate, final String externalKey,
-                           final String email, final String name, final Integer firstNameLength, final Currency currency,
-                           final UUID parentAccountId, final Boolean isPaymentDelegatedToParent,
-                           final int billingCycleDayLocal, final UUID paymentMethodId, final DateTime referenceTime, final DateTimeZone timeZone,
-                           final String locale, final String address1, final String address2, final String companyName,
-                           final String city, final String stateOrProvince, final String country, final String postalCode,
-                           final String phone, final String notes, final Boolean migrated) {
+    private AccountModelDao(final UUID id, final DateTime createdDate, final DateTime updatedDate, final String externalKey,
+                            final String email, final String name, final Integer firstNameLength, final Currency currency,
+                            final UUID parentAccountId, final Boolean isPaymentDelegatedToParent,
+                            final int billingCycleDayLocal, final UUID paymentMethodId, final DateTime referenceTime, final DateTimeZone timeZone,
+                            final String locale, final String address1, final String address2, final String companyName,
+                            final String city, final String stateOrProvince, final String country, final String postalCode,
+                            final String phone, final String notes, final Boolean migrated, final boolean withDefaults) {
         super(id, createdDate, updatedDate);
-        this.externalKey = MoreObjects.firstNonNull(externalKey, id.toString());
+        this.externalKey = !withDefaults ? externalKey : MoreObjects.firstNonNull(externalKey, id.toString());
         this.email = email;
         this.name = name;
         this.firstNameLength = firstNameLength;
@@ -82,7 +82,7 @@ public class AccountModelDao extends EntityModelDaoBase implements TimeZoneAware
         this.billingCycleDayLocal = billingCycleDayLocal;
         this.paymentMethodId = paymentMethodId;
         this.referenceTime = referenceTime;
-        this.timeZone = MoreObjects.firstNonNull(timeZone, DateTimeZone.UTC);
+        this.timeZone = !withDefaults ? timeZone : MoreObjects.firstNonNull(timeZone, DateTimeZone.UTC);
         this.locale = locale;
         this.address1 = address1;
         this.address2 = address2;
@@ -96,7 +96,7 @@ public class AccountModelDao extends EntityModelDaoBase implements TimeZoneAware
         this.migrated = migrated;
     }
 
-    private AccountModelDao(final UUID id, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate, final AccountData account) {
+    public AccountModelDao(final UUID id, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate, final AccountData account, final boolean withDefaults) {
         this(id,
              createdDate,
              updatedDate,
@@ -121,19 +121,101 @@ public class AccountModelDao extends EntityModelDaoBase implements TimeZoneAware
              account.getPostalCode(),
              account.getPhone(),
              account.getNotes(),
-             account.isMigrated());
+             account.isMigrated(),
+             withDefaults);
     }
 
 
     public AccountModelDao(final UUID accountId, final AccountData account) {
-        this(accountId, null, null, account);
+        this(accountId, null, null, account, true);
     }
 
-
     public AccountModelDao(final AccountData account) {
-        this(UUIDs.randomUUID(), null, null, account);
+        this(UUIDs.randomUUID(), null, null, account, true);
+    }
+
+    public void mergeWithDelegate(final AccountModelDao currentAccount) {
+        setExternalKey(currentAccount.getExternalKey());
+
+        setCurrency(currentAccount.getCurrency());
+
+        if (currentAccount.getBillingCycleDayLocal() == DEFAULT_BILLING_CYCLE_DAY_LOCAL && // There is *not* already a BCD set
+            billingCycleDayLocal != DEFAULT_BILLING_CYCLE_DAY_LOCAL) {  // and the proposed date is not 0
+            setBillingCycleDayLocal(billingCycleDayLocal);
+        } else {
+            setBillingCycleDayLocal(currentAccount.getBillingCycleDayLocal());
+        }
+
+        // Set all updatable fields with the new values if non null, otherwise defaults to the current values
+        setEmail(email != null ? email : currentAccount.getEmail());
+        setName(name != null ? name : currentAccount.getName());
+        final Integer firstNameLength = this.firstNameLength != null ? this.firstNameLength : currentAccount.getFirstNameLength();
+        if (firstNameLength != null) {
+            setFirstNameLength(firstNameLength);
+        }
+        setPaymentMethodId(paymentMethodId != null ? paymentMethodId : currentAccount.getPaymentMethodId());
+        setTimeZone(timeZone != null ? timeZone : currentAccount.getTimeZone());
+        setLocale(locale != null ? locale : currentAccount.getLocale());
+        setAddress1(address1 != null ? address1 : currentAccount.getAddress1());
+        setAddress2(address2 != null ? address2 : currentAccount.getAddress2());
+        setCompanyName(companyName != null ? companyName : currentAccount.getCompanyName());
+        setCity(city != null ? city : currentAccount.getCity());
+        setStateOrProvince(stateOrProvince != null ? stateOrProvince : currentAccount.getStateOrProvince());
+        setCountry(country != null ? country : currentAccount.getCountry());
+        setPostalCode(postalCode != null ? postalCode : currentAccount.getPostalCode());
+        setPhone(phone != null ? phone : currentAccount.getPhone());
+        setNotes(notes != null ? notes : currentAccount.getNotes());
+        setParentAccountId(parentAccountId != null ? parentAccountId : currentAccount.getParentAccountId());
+        setIsPaymentDelegatedToParent(isPaymentDelegatedToParent != null ? isPaymentDelegatedToParent : currentAccount.getIsPaymentDelegatedToParent());
+        final Boolean isMigrated = this.migrated != null ? this.migrated : currentAccount.getMigrated();
+        if (isMigrated != null) {
+            setMigrated(isMigrated);
+        }
     }
 
+    public void validateAccountUpdateInput(final AccountModelDao currentAccount, final boolean ignoreNullInput) {
+        //
+        // We don't allow update on the following fields:
+        //
+        // All these conditions are written in the exact same way:
+        //
+        // There is already a defined value BUT those don't match (either input is null or different) => Not Allowed
+        // * ignoreNullInput = false (case where we allow to reset values)
+        // * ignoreNullInput = true (case where we DON'T allow to reset values and so if such value is null we ignore the check)
+        //
+        //
+        if ((ignoreNullInput || externalKey != null) &&
+            currentAccount.getExternalKey() != null &&
+            !currentAccount.getExternalKey().equals(externalKey)) {
+            throw new IllegalArgumentException(String.format("Killbill doesn't support updating the account external key: new=%s, current=%s",
+                                                             externalKey, currentAccount.getExternalKey()));
+        }
+
+        if ((ignoreNullInput || currency != null) &&
+            currentAccount.getCurrency() != null &&
+            !currentAccount.getCurrency().equals(currency)) {
+            throw new IllegalArgumentException(String.format("Killbill doesn't support updating the account currency: new=%s, current=%s",
+                                                             currency, currentAccount.getCurrency()));
+        }
+
+        if ((ignoreNullInput || (billingCycleDayLocal != DEFAULT_BILLING_CYCLE_DAY_LOCAL)) &&
+            currentAccount.getBillingCycleDayLocal() != DEFAULT_BILLING_CYCLE_DAY_LOCAL && // There is already a BCD set
+            !currentAccount.getBillingCycleDayLocal().equals(billingCycleDayLocal)) { // and it does not match we we have
+            throw new IllegalArgumentException(String.format("Killbill doesn't support updating the account BCD: new=%s, current=%s", billingCycleDayLocal, currentAccount.getBillingCycleDayLocal()));
+        }
+
+        if ((ignoreNullInput || timeZone != null) &&
+            currentAccount.getTimeZone() != null &&
+            !currentAccount.getTimeZone().equals(timeZone)) {
+            throw new IllegalArgumentException(String.format("Killbill doesn't support updating the account timeZone: new=%s, current=%s",
+                                                             timeZone, currentAccount.getTimeZone()));
+        }
+
+        if (referenceTime != null && currentAccount.getReferenceTime().withMillisOfDay(0).compareTo(referenceTime.withMillisOfDay(0)) != 0) {
+            throw new IllegalArgumentException(String.format("Killbill doesn't support updating the account referenceTime: new=%s, current=%s",
+                                                             referenceTime, currentAccount.getReferenceTime()));
+        }
+    }
 
     @Override
     public void setRecordId(final Long recordId) {
diff --git a/account/src/main/java/org/killbill/billing/account/dao/DefaultAccountDao.java b/account/src/main/java/org/killbill/billing/account/dao/DefaultAccountDao.java
index 909db49..91cd877 100644
--- a/account/src/main/java/org/killbill/billing/account/dao/DefaultAccountDao.java
+++ b/account/src/main/java/org/killbill/billing/account/dao/DefaultAccountDao.java
@@ -197,7 +197,7 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
     }
 
     @Override
-    public void update(final AccountModelDao specifiedAccount, final InternalCallContext context) throws AccountApiException {
+    public void update(final AccountModelDao specifiedAccount, final boolean treatNullValueAsReset, final InternalCallContext context) throws AccountApiException {
         transactionalSqlDao.execute(false, AccountApiException.class, new EntitySqlDaoTransactionWrapper<Void>() {
             @Override
             public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws EventBusException, AccountApiException {
@@ -209,6 +209,13 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
                     throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_ID, accountId);
                 }
 
+                specifiedAccount.validateAccountUpdateInput(currentAccount, treatNullValueAsReset);
+
+                if (!treatNullValueAsReset) {
+                    // Set unspecified (null) fields to their current values
+                    specifiedAccount.mergeWithDelegate(currentAccount);
+                }
+
                 transactional.update(specifiedAccount, context);
 
                 final AccountChangeInternalEvent changeEvent = new DefaultAccountChangeEvent(accountId,
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 69e8199..72970a3 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
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -81,7 +81,7 @@ public class MockAccountDao extends MockEntityDaoBase<AccountModelDao, Account, 
     }
 
     @Override
-    public void update(final AccountModelDao account, final InternalCallContext context) {
+    public void update(final AccountModelDao account, final boolean treatNullValueAsReset, final InternalCallContext context) {
         super.update(account, context);
 
         final AccountModelDao currentAccount = getById(account.getId(), context);
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 fabbe60..b9790d2 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-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -84,6 +84,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
     public void testBasic() throws AccountApiException {
         final AccountModelDao account = createTestAccount();
         accountDao.create(account, internalCallContext);
+        refreshCallContext(account.getId());
 
         // Retrieve by key
         AccountModelDao retrievedAccount = accountDao.getAccountByKey(account.getExternalKey(), internalCallContext);
@@ -143,6 +144,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
     public void testLongPhoneNumber() throws AccountApiException {
         final AccountModelDao account = createTestAccount("123456789012345678901234");
         accountDao.create(account, internalCallContext);
+        refreshCallContext(account.getId());
 
         final AccountModelDao retrievedAccount = accountDao.getAccountByKey(account.getExternalKey(), internalCallContext);
         checkAccountsEqual(retrievedAccount, account);
@@ -198,6 +200,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
     public void testGetIdFromKey() throws AccountApiException {
         final AccountModelDao account = createTestAccount();
         accountDao.create(account, internalCallContext);
+        refreshCallContext(account.getId());
 
         final UUID accountId = accountDao.getIdFromKey(account.getExternalKey(), internalCallContext);
         Assert.assertEquals(accountId, account.getId());
@@ -212,6 +215,8 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
     public void testUpdate() throws Exception {
         final AccountModelDao account = createTestAccount();
         accountDao.create(account, internalCallContext);
+        refreshCallContext(account.getId());
+
         final AccountModelDao createdAccount = accountDao.getAccountByKey(account.getExternalKey(), internalCallContext);
         List<AuditLogWithHistory> auditLogsWithHistory = accountDao.getAuditLogsWithHistoryForId(account.getId(), AuditLevel.FULL, internalCallContext);
         Assert.assertEquals(auditLogsWithHistory.size(), 1);
@@ -226,11 +231,10 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         Assert.assertEquals(history1.getLocale(), createdAccount.getLocale());
 
         final AccountData accountData = new MockAccountBuilder(new DefaultAccount(account)).migrated(false)
-                                                                                           .timeZone(DateTimeZone.forID("Australia/Darwin"))
                                                                                            .locale("FR-CA")
                                                                                            .build();
         final AccountModelDao updatedAccount = new AccountModelDao(account.getId(), accountData);
-        accountDao.update(updatedAccount, internalCallContext);
+        accountDao.update(updatedAccount, true, internalCallContext);
 
         final AccountModelDao retrievedAccount = accountDao.getAccountByKey(account.getExternalKey(), internalCallContext);
         checkAccountsEqual(retrievedAccount, updatedAccount);
@@ -250,7 +254,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         final AccountData accountData2 = new MockAccountBuilder(new DefaultAccount(updatedAccount)).locale("en_US")
                                                                                                    .build();
         final AccountModelDao updatedAccount2 = new AccountModelDao(account.getId(), accountData2);
-        accountDao.update(updatedAccount2, internalCallContext);
+        accountDao.update(updatedAccount2, true, internalCallContext);
 
         final AccountModelDao retrievedAccount2 = accountDao.getAccountByKey(account.getExternalKey(), internalCallContext);
         checkAccountsEqual(retrievedAccount2, updatedAccount2);
@@ -273,6 +277,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
     public void testUpdatePaymentMethod() throws Exception {
         final AccountModelDao account = createTestAccount();
         accountDao.create(account, internalCallContext);
+        refreshCallContext(account.getId());
 
         final UUID newPaymentMethodId = UUID.randomUUID();
         accountDao.updatePaymentMethod(account.getId(), newPaymentMethodId, internalCallContext);
@@ -291,12 +296,13 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
     public void testShouldBeAbleToUpdateSomeFields() throws Exception {
         final AccountModelDao account = createTestAccount();
         accountDao.create(account, internalCallContext);
+        refreshCallContext(account.getId());
 
         final MutableAccountData otherAccount = new DefaultAccount(account).toMutableAccountData();
         otherAccount.setAddress1(UUID.randomUUID().toString());
         otherAccount.setEmail(UUID.randomUUID().toString());
         final AccountModelDao newAccount = new AccountModelDao(account.getId(), otherAccount);
-        accountDao.update(newAccount, internalCallContext);
+        accountDao.update(newAccount, true, internalCallContext);
 
         final AccountModelDao retrievedAccount = accountDao.getById(account.getId(), internalCallContext);
         checkAccountsEqual(retrievedAccount, newAccount);
@@ -306,6 +312,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
     public void testShouldBeAbleToHandleBCDOfZero() throws Exception {
         final AccountModelDao account = createTestAccount(0);
         accountDao.create(account, internalCallContext);
+        refreshCallContext(account.getId());
 
         // Same BCD (zero)
         final AccountModelDao retrievedAccount = accountDao.getById(account.getId(), internalCallContext);
diff --git a/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java b/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java
index 52d1ec3..18ab04b 100644
--- a/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2012 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -27,6 +29,8 @@ import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
 public interface BillingInternalApi {
 
     /**
+     * Note: this method assumes the lock is taken (https://github.com/killbill/killbill/issues/282)
+     *
      * @return an ordered list of billing event for the given accounts
      */
     public BillingEventSet getBillingEventsForAccountAndUpdateAccountBCD(UUID accountId, DryRunArguments dryRunArguments, InternalCallContext context) throws CatalogApiException, AccountApiException, SubscriptionBaseApiException;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
index d418097..56f11cb 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
@@ -814,7 +814,7 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
 
         // Record usage for first month
         recordUsageData(aoSubscription.getId(), "tracking-1", "bullets", new LocalDate(2012, 4, 5), 100L, callContext);
-        recordUsageData(aoSubscription.getId(), "tracking-1", "bullets", new LocalDate(2012, 4, 15), 100L, callContext);
+        recordUsageData(aoSubscription.getId(), "tracking-2", "bullets", new LocalDate(2012, 4, 15), 100L, callContext);
 
         // 2012-05-01
         busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
@@ -843,8 +843,8 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 5), InvoiceItemType.USAGE, BigDecimal.ZERO));
 
         // Record usage for second month
-        recordUsageData(aoSubscription.getId(), "tracking-1", "bullets", new LocalDate(2012, 5, 5), 100L, callContext);
-        recordUsageData(aoSubscription.getId(), "tracking-1", "bullets", new LocalDate(2012, 6, 4), 100L, callContext);
+        recordUsageData(aoSubscription.getId(), "tracking-3", "bullets", new LocalDate(2012, 5, 5), 100L, callContext);
+        recordUsageData(aoSubscription.getId(), "tracking-4", "bullets", new LocalDate(2012, 6, 4), 100L, callContext);
 
         // 2012-06-01
         busHandler.pushExpectedEvents(NextEvent.NULL_INVOICE);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index 271b15c..767cd30 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -174,9 +174,7 @@ public class InvoiceDispatcher {
         this.parkedAccountsManager = parkedAccountsManager;
     }
 
-
     public void processSubscriptionStartRequestedDate(final RequestedSubscriptionInternalEvent transition, final InternalCallContext context) {
-
         final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule(context).getMillis();
         final boolean isInvoiceNotificationEnabled = dryRunNotificationTime > 0;
         if (!isInvoiceNotificationEnabled) {
@@ -186,16 +184,28 @@ public class InvoiceDispatcher {
         final UUID accountId;
         try {
             accountId = subscriptionApi.getAccountIdFromSubscriptionId(transition.getSubscriptionId(), context);
-
-
         } catch (final SubscriptionBaseApiException e) {
             log.warn("Failed handling SubscriptionBase change.",
                      new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, transition.getSubscriptionId().toString()));
             return;
         }
 
+        GlobalLock lock = null;
         try {
+            lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), invoiceConfig.getMaxGlobalLockRetries());
+
+            processSubscriptionStartRequestedDateWithLock(accountId, transition, context);
+        } catch (final LockFailedException e) {
+            log.warn("Failed to process RequestedSubscriptionInternalEvent for accountId='{}'", accountId.toString(), e);
+        } finally {
+            if (lock != null) {
+                lock.release();
+            }
+        }
+    }
 
+    private void processSubscriptionStartRequestedDateWithLock(final UUID accountId, final RequestedSubscriptionInternalEvent transition, final InternalCallContext context) {
+        try {
             final BillingEventSet billingEvents = billingApi.getBillingEventsForAccountAndUpdateAccountBCD(accountId, null, context);
             if (billingEvents.isEmpty()) {
                 return;
@@ -207,7 +217,6 @@ public class InvoiceDispatcher {
             final ImmutableAccountData account = accountApi.getImmutableAccountDataById(accountId, context);
 
             commitInvoiceAndSetFutureNotifications(account, notificationsBuilder.build(), context);
-
         } catch (final SubscriptionBaseApiException e) {
             log.warn("Failed handling SubscriptionBase change.",
                      new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, transition.getSubscriptionId().toString()));
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountJson.java
index d72ec1d..4711e07 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountJson.java
@@ -297,7 +297,7 @@ public class AccountJson extends JsonBase {
 
             @Override
             public Account mergeWithDelegate(final Account delegate) {
-                return null;
+                throw new UnsupportedOperationException();
             }
         };
     }
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
index 50c2af3..a096e5e 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
@@ -139,12 +139,35 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
         }
     }
 
-    private void addBillingEventsForBundles(final List<SubscriptionBaseBundle> bundles, final ImmutableAccountData account, final DryRunArguments dryRunArguments, final InternalCallContext context,
-                                            final DefaultBillingEventSet result, final Set<UUID> skipSubscriptionsSet, final Catalog catalog, final List<Tag> tagsForAccount) throws AccountApiException, CatalogApiException, SubscriptionBaseApiException {
-        final boolean dryRunMode = dryRunArguments != null;
-
+    private void addBillingEventsForBundles(final List<SubscriptionBaseBundle> bundles,
+                                            final ImmutableAccountData account,
+                                            final DryRunArguments dryRunArguments,
+                                            final InternalCallContext context,
+                                            final DefaultBillingEventSet result,
+                                            final Set<UUID> skipSubscriptionsSet,
+                                            final Catalog catalog,
+                                            final List<Tag> tagsForAccount) throws AccountApiException, CatalogApiException, SubscriptionBaseApiException {
         final int currentAccountBCD = accountApi.getBCD(context);
+        addBillingEventsForBundles(bundles,
+                                   account,
+                                   dryRunArguments,
+                                   context,
+                                   result,
+                                   skipSubscriptionsSet,
+                                   catalog,
+                                   tagsForAccount,
+                                   currentAccountBCD);
+    }
 
+    private void addBillingEventsForBundles(final List<SubscriptionBaseBundle> bundles,
+                                            final ImmutableAccountData account,
+                                            final DryRunArguments dryRunArguments,
+                                            final InternalCallContext context,
+                                            final DefaultBillingEventSet result,
+                                            final Set<UUID> skipSubscriptionsSet,
+                                            final Catalog catalog,
+                                            final List<Tag> tagsForAccount,
+                                            final int currentAccountBCD) throws AccountApiException, CatalogApiException, SubscriptionBaseApiException {
         // In dryRun mode, when we care about invoice generated for new BASE subscription, no such bundle exists yet; we still
         // want to tap into subscriptionBase logic, so we make up a bundleId
         if (dryRunArguments != null &&
@@ -185,46 +208,58 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
         }
 
         // If dryRun is specified, we don't want to to update the account BCD value, so we initialize the flag updatedAccountBCD to true
-        if (currentAccountBCD == 0 && !dryRunMode) {
-            BillingEvent oldestAccountAlignedBillingEvent = null;
-
-            for (final BillingEvent event : result) {
-                if (event.getBillingAlignment() != BillingAlignment.ACCOUNT) {
-                    continue;
-                }
+        if (currentAccountBCD == 0) {
+            final Integer accountBCDCandidate = computeAccountBCD(result);
+            if (accountBCDCandidate == null) {
+                return;
+            }
 
-                final BigDecimal recurringPrice = event.getRecurringPrice(event.getEffectiveDate());
-                final boolean hasRecurringPrice = recurringPrice != null; // Note: could be zero (BCD would still be set, by convention)
-                final boolean hasUsage = event.getUsages() != null && !event.getUsages().isEmpty();
-                if (!hasRecurringPrice &&
-                    !hasUsage) {
-                    // Nothing to bill, ignored for the purpose of BCD calculation
-                    continue;
-                }
+            // Because we now have computed the real BCD, we need to re-compute the BillingEvents BCD for ACCOUNT alignments (see BillCycleDayCalculator#calculateBcdForAlignment).
+            // The code could maybe be optimized (no need to re-run the full function?), but since it's run once per account, it's probably not worth it.
+            result.clear();
+            addBillingEventsForBundles(bundles, account, dryRunArguments, context, result, skipSubscriptionsSet, catalog, tagsForAccount, accountBCDCandidate);
 
-                if (oldestAccountAlignedBillingEvent == null ||
-                    event.getEffectiveDate().compareTo(oldestAccountAlignedBillingEvent.getEffectiveDate()) < 0 ||
-                    (event.getEffectiveDate().compareTo(oldestAccountAlignedBillingEvent.getEffectiveDate()) == 0 && event.getTotalOrdering().compareTo(oldestAccountAlignedBillingEvent.getTotalOrdering()) < 0)) {
-                    oldestAccountAlignedBillingEvent = event;
-                }
+            final boolean dryRunMode = dryRunArguments != null;
+            if (!dryRunMode) {
+                log.info("Setting account BCD='{}', accountId='{}'", accountBCDCandidate, account.getId());
+                accountApi.updateBCD(account.getExternalKey(), accountBCDCandidate, context);
             }
+        }
+    }
 
-            if (oldestAccountAlignedBillingEvent == null) {
-                return;
+    private Integer computeAccountBCD(final BillingEventSet result) throws CatalogApiException {
+        BillingEvent oldestAccountAlignedBillingEvent = null;
+
+        for (final BillingEvent event : result) {
+            if (event.getBillingAlignment() != BillingAlignment.ACCOUNT) {
+                continue;
             }
 
-            // BCD in the account timezone
-            final int accountBCDCandidate = oldestAccountAlignedBillingEvent.getBillCycleDayLocal();
-            Preconditions.checkState(accountBCDCandidate > 0, "Wrong Account BCD calculation for event: " + oldestAccountAlignedBillingEvent);
+            final BigDecimal recurringPrice = event.getRecurringPrice(event.getEffectiveDate());
+            final boolean hasRecurringPrice = recurringPrice != null; // Note: could be zero (BCD would still be set, by convention)
+            final boolean hasUsage = event.getUsages() != null && !event.getUsages().isEmpty();
+            if (!hasRecurringPrice &&
+                !hasUsage) {
+                // Nothing to bill, ignored for the purpose of BCD calculation
+                continue;
+            }
 
-            log.info("Setting account BCD='{}', accountId='{}'", accountBCDCandidate, account.getId());
-            accountApi.updateBCD(account.getExternalKey(), accountBCDCandidate, context);
+            if (oldestAccountAlignedBillingEvent == null ||
+                event.getEffectiveDate().compareTo(oldestAccountAlignedBillingEvent.getEffectiveDate()) < 0 ||
+                (event.getEffectiveDate().compareTo(oldestAccountAlignedBillingEvent.getEffectiveDate()) == 0 && event.getTotalOrdering().compareTo(oldestAccountAlignedBillingEvent.getTotalOrdering()) < 0)) {
+                oldestAccountAlignedBillingEvent = event;
+            }
+        }
 
-            // Because we now have computed the real BCD, we need to re-compute the BillingEvents BCD for ACCOUNT alignments (see BillCycleDayCalculator#calculateBcdForAlignment).
-            // The code could maybe be optimized (no need to re-run the full function?), but since it's run once per account, it's probably not worth it.
-            result.clear();
-            addBillingEventsForBundles(bundles, account, dryRunArguments, context, result, skipSubscriptionsSet, catalog, tagsForAccount);
+        if (oldestAccountAlignedBillingEvent == null) {
+            return null;
         }
+
+        // BCD in the account timezone
+        final int accountBCDCandidate = oldestAccountAlignedBillingEvent.getBillCycleDayLocal();
+        Preconditions.checkState(accountBCDCandidate > 0, "Wrong Account BCD calculation for event: " + oldestAccountAlignedBillingEvent);
+
+        return accountBCDCandidate;
     }
 
     private void addBillingEventsForSubscription(final ImmutableAccountData account,