diff --git a/.travis.yml b/.travis.yml
index e8847be803..dfb5cdc785 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -32,16 +32,6 @@ matrix:
jdk: oraclejdk7
- env: PHASE="-Pmysql,jdk18"
jdk: oraclejdk8
- - env: PHASE="-Ppostgresql"
- jdk: openjdk7
- - env: PHASE="-Ppostgresql"
- jdk: oraclejdk7
- - env: PHASE="-Ppostgresql,jdk17"
- jdk: openjdk7
- - env: PHASE="-Ppostgresql,jdk17"
- jdk: oraclejdk7
- - env: PHASE="-Ppostgresql,jdk18"
- jdk: oraclejdk8
- env: PHASE="-Ptravis"
jdk: openjdk7
- env: PHASE="-Ptravis"
diff --git a/NEWS b/NEWS
index c5b5ee6b99..fc4aaecdb7 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,15 @@
+0.15.6
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.15.6
+
+0.15.5
+ Fix with issue with payment combo call where default account fields are not part of the request
+
+0.15.4
+ See https://github.com/killbill/killbill/issues?q=milestone%3ARelease-0.15.4+is%3Aclosed
+
+0.15.3
+ Release for milestone 0.15.3: https://github.com/killbill/killbill/issues?q=is%3Aissue+milestone%3ARelease-0.15.3+is%3Aclosed
+
0.15.2
See https://github.com/killbill/killbill/issues?q=milestone%3ARelease-0.15.2+is%3Aclosed
diff --git a/account/pom.xml b/account/pom.xml
index e331e15af2..9c9e729160 100644
--- a/account/pom.xml
+++ b/account/pom.xml
@@ -19,7 +19,7 @@
killbill
org.kill-bill.billing
- 0.15.3-SNAPSHOT
+ 0.15.7-SNAPSHOT
../pom.xml
killbill-account
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 25384b3643..957b2e2959 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
@@ -295,15 +295,18 @@ public Account mergeWithDelegate(final Account currentAccount) {
accountData.setCurrency(currentAccount.getCurrency());
}
- if (billCycleDayLocal != null && billCycleDayLocal != DEFAULT_BILLING_CYCLE_DAY_LOCAL && currentAccount.getBillCycleDayLocal() != null && currentAccount.getBillCycleDayLocal() != DEFAULT_BILLING_CYCLE_DAY_LOCAL && !billCycleDayLocal.equals(currentAccount.getBillCycleDayLocal())) {
- throw new IllegalArgumentException(String.format("Killbill doesn't support updating the account BCD yet: new=%s, current=%s",
- billCycleDayLocal, currentAccount.getBillCycleDayLocal()));
- } else if (billCycleDayLocal != null && billCycleDayLocal != DEFAULT_BILLING_CYCLE_DAY_LOCAL) {
- // Junction sets it
+
+ if (currentAccount.getBillCycleDayLocal() != DEFAULT_BILLING_CYCLE_DAY_LOCAL && // There is already a BCD set
+ billCycleDayLocal != null && // and the proposed date is not null
+ billCycleDayLocal != DEFAULT_BILLING_CYCLE_DAY_LOCAL && // and the proposed date is not 0
+ !currentAccount.getBillCycleDayLocal().equals(billCycleDayLocal)) { // and it does not match we we have
+ throw new IllegalArgumentException(String.format("Killbill doesn't support updating the account BCD yet: new=%s, current=%s", billCycleDayLocal, currentAccount.getBillCycleDayLocal()));
+ } else if (currentAccount.getBillCycleDayLocal() == DEFAULT_BILLING_CYCLE_DAY_LOCAL && // There is *not* already a BCD set
+ billCycleDayLocal != null && // and the value proposed is not null
+ billCycleDayLocal != DEFAULT_BILLING_CYCLE_DAY_LOCAL) { // and the proposed date is not 0
accountData.setBillCycleDayLocal(billCycleDayLocal);
} else {
- // Default to current value
- accountData.setBillCycleDayLocal(currentAccount.getBillCycleDayLocal() == null ? DEFAULT_BILLING_CYCLE_DAY_LOCAL : currentAccount.getBillCycleDayLocal());
+ accountData.setBillCycleDayLocal(currentAccount.getBillCycleDayLocal());
}
// Set all updatable fields with the new values if non null, otherwise defaults to the current values
@@ -336,6 +339,10 @@ public Account mergeWithDelegate(final Account currentAccount) {
return new DefaultAccount(currentAccount.getId(), accountData);
}
+ public ImmutableAccountData toImmutableAccountData() {
+ return new DefaultImmutableAccountData(this);
+ }
+
@Override
public String toString() {
return "DefaultAccount [externalKey=" + externalKey +
diff --git a/account/src/main/java/org/killbill/billing/account/api/DefaultImmutableAccountData.java b/account/src/main/java/org/killbill/billing/account/api/DefaultImmutableAccountData.java
new file mode 100644
index 0000000000..e1df215c00
--- /dev/null
+++ b/account/src/main/java/org/killbill/billing/account/api/DefaultImmutableAccountData.java
@@ -0,0 +1,62 @@
+/*
+ * 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.account.api;
+
+import java.util.UUID;
+
+import org.joda.time.DateTimeZone;
+import org.killbill.billing.catalog.api.Currency;
+
+public class DefaultImmutableAccountData implements ImmutableAccountData {
+
+ private final UUID id;
+ private final String externalKey;
+ private final Currency currency;
+ private final DateTimeZone dateTimeZone;
+
+ public DefaultImmutableAccountData(final UUID id, final String externalKey, final Currency currency, final DateTimeZone dateTimeZone) {
+ this.id = id;
+ this.externalKey = externalKey;
+ this.currency = currency;
+ this.dateTimeZone = dateTimeZone;
+ }
+
+ public DefaultImmutableAccountData(final Account account) {
+ this(account.getId(), account.getExternalKey(), account.getCurrency(), account.getTimeZone());
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public String getExternalKey() {
+ return externalKey;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public DateTimeZone getTimeZone() {
+ return dateTimeZone;
+ }
+}
diff --git a/api/src/main/java/org/killbill/billing/account/api/DefaultMutableAccountData.java b/account/src/main/java/org/killbill/billing/account/api/DefaultMutableAccountData.java
similarity index 100%
rename from api/src/main/java/org/killbill/billing/account/api/DefaultMutableAccountData.java
rename to account/src/main/java/org/killbill/billing/account/api/DefaultMutableAccountData.java
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 5502b4e93b..0903c13545 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
@@ -22,65 +22,95 @@
import javax.inject.Inject;
import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
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;
import org.killbill.billing.account.api.AccountInternalApi;
-import org.killbill.billing.account.api.DefaultAccount;
import org.killbill.billing.account.api.DefaultAccountEmail;
+import org.killbill.billing.account.api.DefaultImmutableAccountData;
+import org.killbill.billing.account.api.DefaultMutableAccountData;
+import org.killbill.billing.account.api.ImmutableAccountData;
+import org.killbill.billing.account.api.MutableAccountData;
+import org.killbill.billing.account.api.user.DefaultAccountApiBase;
import org.killbill.billing.account.dao.AccountDao;
import org.killbill.billing.account.dao.AccountEmailModelDao;
import org.killbill.billing.account.dao.AccountModelDao;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
-import org.killbill.billing.util.entity.DefaultPagination;
-import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.cache.AccountBCDCacheLoader;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.cache.CacheLoaderArgument;
+import org.killbill.billing.util.cache.ImmutableAccountCacheLoader.LoaderCallback;
+import org.killbill.billing.util.dao.NonEntityDao;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterators;
-public class DefaultAccountInternalApi implements AccountInternalApi {
+public class DefaultAccountInternalApi extends DefaultAccountApiBase implements AccountInternalApi {
private final AccountDao accountDao;
+ private final CacheControllerDispatcher cacheControllerDispatcher;
+ private final CacheController accountCacheController;
+ private final CacheController bcdCacheController;
+ private final NonEntityDao nonEntityDao;
@Inject
- public DefaultAccountInternalApi(final AccountDao accountDao) {
+ public DefaultAccountInternalApi(final AccountDao accountDao,
+ final NonEntityDao nonEntityDao,
+ final CacheControllerDispatcher cacheControllerDispatcher) {
+ super(accountDao, nonEntityDao, cacheControllerDispatcher);
this.accountDao = accountDao;
+ this.nonEntityDao = nonEntityDao;
+ this.cacheControllerDispatcher = cacheControllerDispatcher;
+ this.accountCacheController = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_IMMUTABLE);
+ this.bcdCacheController = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_BCD);
}
@Override
public Account getAccountById(final UUID accountId, final InternalTenantContext context) throws AccountApiException {
- final AccountModelDao account = accountDao.getById(accountId, context);
- if (account == null) {
- throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_ID, accountId);
- }
- return new DefaultAccount(account);
+ return super.getAccountById(accountId, context);
+ }
+
+ @Override
+ public Account getAccountByKey(final String key, final InternalTenantContext context) throws AccountApiException {
+ return super.getAccountByKey(key, context);
}
@Override
public Account getAccountByRecordId(final Long recordId, final InternalTenantContext context) throws AccountApiException {
- final AccountModelDao accountModelDao = getAccountModelDaoByRecordId(recordId, context);
- return new DefaultAccount(accountModelDao);
+ return super.getAccountByRecordId(recordId, context);
}
@Override
- public void updateAccount(final String externalKey, final AccountData accountData,
- final InternalCallContext context) throws AccountApiException {
+ public void updateBCD(final String externalKey, final int bcd,
+ final InternalCallContext context) throws AccountApiException {
final Account currentAccount = getAccountByKey(externalKey, context);
if (currentAccount == null) {
throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_KEY, externalKey);
}
+ if (currentAccount.getBillCycleDayLocal() != DefaultMutableAccountData.DEFAULT_BILLING_CYCLE_DAY_LOCAL) {
+ throw new AccountApiException(ErrorCode.ACCOUNT_UPDATE_FAILED);
+ }
- // Set unspecified (null) fields to their current values
- final Account updatedAccount = new DefaultAccount(currentAccount.getId(), accountData);
- final AccountModelDao accountToUpdate = new AccountModelDao(currentAccount.getId(), updatedAccount.mergeWithDelegate(currentAccount));
-
+ final MutableAccountData mutableAccountData = currentAccount.toMutableAccountData();
+ mutableAccountData.setBillCycleDayLocal(bcd);
+ final AccountModelDao accountToUpdate = new AccountModelDao(currentAccount.getId(), mutableAccountData);
+ bcdCacheController.remove(currentAccount.getId());
+ bcdCacheController.putIfAbsent(currentAccount.getId(), new Integer(bcd));
accountDao.update(accountToUpdate, context);
}
+ @Override
+ public int getBCD(final UUID accountId, final InternalTenantContext context) throws AccountApiException {
+ final CacheLoaderArgument arg = createBCDCacheLoaderArgument(context);
+ final Integer result = (Integer) bcdCacheController.get(accountId, arg);
+ return result != null ? result : DefaultMutableAccountData.DEFAULT_BILLING_CYCLE_DAY_LOCAL;
+ }
+
@Override
public List getEmails(final UUID accountId,
final InternalTenantContext context) {
@@ -93,15 +123,6 @@ public AccountEmail apply(final AccountEmailModelDao input) {
}));
}
- @Override
- public Account getAccountByKey(final String key, final InternalTenantContext context) throws AccountApiException {
- final AccountModelDao accountModelDao = accountDao.getAccountByKey(key, context);
- if (accountModelDao == null) {
- throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_KEY, key);
- }
- return new DefaultAccount(accountModelDao);
- }
-
@Override
public void removePaymentMethod(final UUID accountId, final InternalCallContext context) throws AccountApiException {
updatePaymentMethod(accountId, null, context);
@@ -119,6 +140,18 @@ public UUID getByRecordId(final Long recordId, final InternalTenantContext conte
return accountModelDao.getId();
}
+ @Override
+ public ImmutableAccountData getImmutableAccountDataById(final UUID accountId, final InternalTenantContext context) throws AccountApiException {
+ final Long recordId = nonEntityDao.retrieveRecordIdFromObject(accountId, ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID));
+ return getImmutableAccountDataByRecordId(recordId, context);
+ }
+
+ @Override
+ public ImmutableAccountData getImmutableAccountDataByRecordId(final Long recordId, final InternalTenantContext context) throws AccountApiException {
+ final CacheLoaderArgument arg = createImmutableAccountCacheLoaderArgument(context);
+ return (ImmutableAccountData) accountCacheController.get(recordId, arg);
+ }
+
private AccountModelDao getAccountModelDaoByRecordId(final Long recordId, final InternalTenantContext context) throws AccountApiException {
final AccountModelDao accountModelDao = accountDao.getByRecordId(recordId, context);
if (accountModelDao == null) {
@@ -126,4 +159,37 @@ private AccountModelDao getAccountModelDaoByRecordId(final Long recordId, final
}
return accountModelDao;
}
+
+ private int getBCDInternal(final UUID accountId, final InternalTenantContext context) {
+ final Integer bcd = accountDao.getAccountBCD(accountId, context);
+ return bcd != null ? bcd : DefaultMutableAccountData.DEFAULT_BILLING_CYCLE_DAY_LOCAL;
+ }
+
+ private CacheLoaderArgument createImmutableAccountCacheLoaderArgument(final InternalTenantContext context) {
+ final LoaderCallback loaderCallback = new LoaderCallback() {
+ @Override
+ public Object loadAccount(final Long recordId, final InternalTenantContext context) {
+ final Account account = getAccountByRecordIdInternal(recordId, context);
+ return account != null ? new DefaultImmutableAccountData(account) : null;
+ }
+ };
+ final Object[] args = new Object[1];
+ args[0] = loaderCallback;
+ final ObjectType irrelevant = null;
+ return new CacheLoaderArgument(irrelevant, args, context);
+ }
+
+ private CacheLoaderArgument createBCDCacheLoaderArgument(final InternalTenantContext context) {
+ final AccountBCDCacheLoader.LoaderCallback loaderCallback = new AccountBCDCacheLoader.LoaderCallback() {
+ @Override
+ public Object loadAccountBCD(final UUID accountId, final InternalTenantContext context) {
+ int bcd = getBCDInternal(accountId, context);
+ return new Integer(bcd);
+ }
+ };
+ final Object[] args = new Object[1];
+ args[0] = loaderCallback;
+ final ObjectType irrelevant = null;
+ return new CacheLoaderArgument(irrelevant, args, context);
+ }
}
diff --git a/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountApiBase.java b/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountApiBase.java
new file mode 100644
index 0000000000..7792ab4afc
--- /dev/null
+++ b/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountApiBase.java
@@ -0,0 +1,84 @@
+/*
+ * 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.account.api.user;
+
+import java.util.UUID;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.DefaultAccount;
+import org.killbill.billing.account.api.DefaultImmutableAccountData;
+import org.killbill.billing.account.dao.AccountDao;
+import org.killbill.billing.account.dao.AccountModelDao;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+public class DefaultAccountApiBase {
+
+ private final AccountDao accountDao;
+ private final CacheControllerDispatcher cacheControllerDispatcher;
+ private final CacheController accountCacheController;
+ private final NonEntityDao nonEntityDao;
+
+ public DefaultAccountApiBase(final AccountDao accountDao,
+ final NonEntityDao nonEntityDao,
+ final CacheControllerDispatcher cacheControllerDispatcher) {
+ this.accountDao = accountDao;
+ this.nonEntityDao = nonEntityDao;
+ this.cacheControllerDispatcher = cacheControllerDispatcher;
+ this.accountCacheController = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_IMMUTABLE);
+ }
+
+ protected Account getAccountById(final UUID accountId, final InternalTenantContext context) throws AccountApiException {
+ final Long recordId = nonEntityDao.retrieveRecordIdFromObject(accountId, ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID));
+ final Account account = getAccountByRecordIdInternal(recordId, context);
+ if (account == null) {
+ throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_ID, accountId);
+ }
+ accountCacheController.putIfAbsent(accountId, new DefaultImmutableAccountData(account));
+ return account;
+ }
+
+ protected Account getAccountByKey(final String key, final InternalTenantContext context) throws AccountApiException {
+ final AccountModelDao accountModelDao = accountDao.getAccountByKey(key, context);
+ if (accountModelDao == null) {
+ throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_KEY, key);
+ }
+ final Account account = new DefaultAccount(accountModelDao);
+ accountCacheController.putIfAbsent(account.getId(), new DefaultImmutableAccountData(account));
+ return account;
+ }
+
+ protected Account getAccountByRecordId(final Long recordId, final InternalTenantContext context) throws AccountApiException {
+ final Account account = getAccountByRecordIdInternal(recordId, context);
+ if (account == null) {
+ throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_RECORD_ID, recordId);
+ }
+ return account;
+ }
+
+ protected Account getAccountByRecordIdInternal(final Long recordId, final InternalTenantContext context) {
+ final AccountModelDao accountModelDao = accountDao.getByRecordId(recordId, context);
+ return accountModelDao != null ? new DefaultAccount(accountModelDao) : null;
+ }
+}
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 55fb71b7ac..b137cc04b0 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
@@ -32,9 +32,12 @@
import org.killbill.billing.account.dao.AccountDao;
import org.killbill.billing.account.dao.AccountEmailModelDao;
import org.killbill.billing.account.dao.AccountModelDao;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
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.dao.NonEntityDao;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
@@ -45,17 +48,35 @@
import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationNoException;
-public class DefaultAccountUserApi implements AccountUserApi {
+public class DefaultAccountUserApi extends DefaultAccountApiBase implements AccountUserApi {
private final InternalCallContextFactory internalCallContextFactory;
private final AccountDao accountDao;
@Inject
- public DefaultAccountUserApi(final InternalCallContextFactory internalCallContextFactory, final AccountDao accountDao) {
+ public DefaultAccountUserApi(final AccountDao accountDao,
+ final NonEntityDao nonEntityDao,
+ final CacheControllerDispatcher cacheControllerDispatcher,
+ final InternalCallContextFactory internalCallContextFactory) {
+ super(accountDao, nonEntityDao, cacheControllerDispatcher);
this.internalCallContextFactory = internalCallContextFactory;
this.accountDao = accountDao;
}
+
+ @Override
+ public Account getAccountByKey(final String key, final TenantContext context) throws AccountApiException {
+ final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(context);
+ return getAccountByKey(key, internalTenantContext);
+ }
+
+ @Override
+ public Account getAccountById(final UUID id, final TenantContext context) throws AccountApiException {
+ final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(context);
+ return getAccountById(id, internalTenantContext);
+ }
+
+
@Override
public Account createAccount(final AccountData data, final CallContext context) throws AccountApiException {
// Not transactional, but there is a db constraint on that column
@@ -69,25 +90,6 @@ public Account createAccount(final AccountData data, final CallContext context)
return new DefaultAccount(account);
}
- @Override
- public Account getAccountByKey(final String key, final TenantContext context) throws AccountApiException {
- final AccountModelDao account = accountDao.getAccountByKey(key, internalCallContextFactory.createInternalTenantContext(context));
- if (account == null) {
- throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_KEY, key);
- }
-
- return new DefaultAccount(account);
- }
-
- @Override
- public Account getAccountById(final UUID id, final TenantContext context) throws AccountApiException {
- final AccountModelDao account = accountDao.getById(id, internalCallContextFactory.createInternalTenantContext(context));
- if (account == null) {
- throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_ID, id);
- }
-
- return new DefaultAccount(account);
- }
@Override
public Pagination searchAccounts(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
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 2b79ca2036..c5cae6d18d 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
@@ -28,26 +28,28 @@
public interface AccountDao extends EntityDao {
- public AccountModelDao getAccountByKey(String key, InternalTenantContext context);
+ AccountModelDao getAccountByKey(String key, InternalTenantContext context);
- public Pagination searchAccounts(String searchKey, Long offset, Long limit, InternalTenantContext context);
+ Pagination searchAccounts(String searchKey, Long offset, Long limit, InternalTenantContext context);
/**
* @throws AccountApiException when externalKey is null
*/
- public UUID getIdFromKey(String externalKey, InternalTenantContext context) throws AccountApiException;
+ UUID getIdFromKey(String externalKey, InternalTenantContext context) throws AccountApiException;
/**
* @param accountId the id of the account
* @param paymentMethodId the is of the current default paymentMethod
*/
- public void updatePaymentMethod(UUID accountId, UUID paymentMethodId, InternalCallContext context) throws AccountApiException;
+ void updatePaymentMethod(UUID accountId, UUID paymentMethodId, InternalCallContext context) throws AccountApiException;
- public void update(AccountModelDao account, InternalCallContext context) throws AccountApiException;
+ void update(AccountModelDao account, InternalCallContext context) throws AccountApiException;
- public void addEmail(AccountEmailModelDao email, InternalCallContext context) throws AccountApiException;
+ void addEmail(AccountEmailModelDao email, InternalCallContext context) throws AccountApiException;
- public void removeEmail(AccountEmailModelDao email, InternalCallContext context);
+ void removeEmail(AccountEmailModelDao email, InternalCallContext context);
- public List getEmailsByAccountId(UUID accountId, InternalTenantContext context);
+ List getEmailsByAccountId(UUID accountId, InternalTenantContext context);
+
+ Integer getAccountBCD(UUID accountId, InternalTenantContext context);
}
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 353812eaaa..ac703624f2 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
@@ -74,7 +74,7 @@ public AccountModelDao(final UUID id, final DateTime createdDate, final DateTime
this.currency = currency;
this.billingCycleDayLocal = billingCycleDayLocal;
this.paymentMethodId = paymentMethodId;
- this.timeZone = timeZone;
+ this.timeZone = MoreObjects.firstNonNull(timeZone, DateTimeZone.UTC);
this.locale = locale;
this.address1 = address1;
this.address2 = address2;
diff --git a/account/src/main/java/org/killbill/billing/account/dao/AccountSqlDao.java b/account/src/main/java/org/killbill/billing/account/dao/AccountSqlDao.java
index 8eeff93b8b..5395c85fa4 100644
--- a/account/src/main/java/org/killbill/billing/account/dao/AccountSqlDao.java
+++ b/account/src/main/java/org/killbill/billing/account/dao/AccountSqlDao.java
@@ -18,11 +18,6 @@
import java.util.UUID;
-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;
-
import org.killbill.billing.account.api.Account;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
@@ -30,6 +25,10 @@
import org.killbill.billing.util.entity.dao.Audited;
import org.killbill.billing.util.entity.dao.EntitySqlDao;
import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+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;
@EntitySqlDaoStringTemplate
public interface AccountSqlDao extends EntitySqlDao {
@@ -42,6 +41,10 @@ public AccountModelDao getAccountByKey(@Bind("externalKey") final String key,
public UUID getIdFromKey(@Bind("externalKey") final String key,
@BindBean final InternalTenantContext context);
+ @SqlQuery
+ public Integer getBCD(@Bind("id") String accountId,
+ @BindBean final InternalTenantContext context);
+
@SqlUpdate
@Audited(ChangeType.UPDATE)
public void update(@BindBean final AccountModelDao account,
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 b14c677a99..31c33e3ac2 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
@@ -246,4 +246,13 @@ public List inTransaction(final EntitySqlDaoWrapperFactory
});
}
+ @Override
+ public Integer getAccountBCD(final UUID accountId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper() {
+ @Override
+ public Integer inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(AccountSqlDao.class).getBCD(accountId.toString(), context);
+ }
+ });
+ }
}
diff --git a/account/src/main/resources/org/killbill/billing/account/dao/AccountSqlDao.sql.stg b/account/src/main/resources/org/killbill/billing/account/dao/AccountSqlDao.sql.stg
index 0f5bb37dbd..a5000281dc 100644
--- a/account/src/main/resources/org/killbill/billing/account/dao/AccountSqlDao.sql.stg
+++ b/account/src/main/resources/org/killbill/billing/account/dao/AccountSqlDao.sql.stg
@@ -86,6 +86,12 @@ getAccountByKey() ::= <<
where external_key = :externalKey ;
>>
+getBCD() ::= <<
+ select billing_cycle_day_local
+ from accounts
+ where id = :id ;
+>>
+
searchQuery(prefix) ::= <<
= :searchKey
or name like :likeSearchKey
diff --git a/account/src/main/resources/org/killbill/billing/account/ddl.sql b/account/src/main/resources/org/killbill/billing/account/ddl.sql
index 50227ce515..01158e4a77 100644
--- a/account/src/main/resources/org/killbill/billing/account/ddl.sql
+++ b/account/src/main/resources/org/killbill/billing/account/ddl.sql
@@ -11,7 +11,7 @@ CREATE TABLE accounts (
currency varchar(3) DEFAULT NULL,
billing_cycle_day_local int DEFAULT NULL,
payment_method_id varchar(36) DEFAULT NULL,
- time_zone varchar(50) DEFAULT NULL,
+ time_zone varchar(50) NOT NULL,
locale varchar(5) DEFAULT NULL,
address1 varchar(100) DEFAULT NULL,
address2 varchar(100) DEFAULT NULL,
@@ -46,7 +46,7 @@ CREATE TABLE account_history (
currency varchar(3) DEFAULT NULL,
billing_cycle_day_local int DEFAULT NULL,
payment_method_id varchar(36) DEFAULT NULL,
- time_zone varchar(50) DEFAULT NULL,
+ time_zone varchar(50) NOT NULL,
locale varchar(5) DEFAULT NULL,
address1 varchar(100) DEFAULT NULL,
address2 varchar(100) DEFAULT NULL,
diff --git a/account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApiWithMocks.java b/account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApiWithMocks.java
index 1843b08dd8..f3d7863a55 100644
--- a/account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApiWithMocks.java
+++ b/account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApiWithMocks.java
@@ -53,7 +53,7 @@ public class TestDefaultAccountUserApiWithMocks extends AccountTestSuiteNoDB {
@BeforeMethod(groups = "fast")
public void setUp() throws Exception {
accountDao = new MockAccountDao(Mockito.mock(PersistentBus.class));
- accountUserApi = new DefaultAccountUserApi(internalFactory, accountDao);
+ accountUserApi = new DefaultAccountUserApi(accountDao, nonEntityDao, controllerDispatcher, internalFactory);
}
@Test(groups = "fast", description = "Test Account create API")
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 04a8bbe291..e9715ddd5a 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
@@ -163,4 +163,10 @@ public boolean apply(final AccountEmailModelDao input) {
}));
}
+ @Override
+ public Integer getAccountBCD(final UUID accountId, final InternalTenantContext context) {
+ final AccountModelDao account = getById(accountId, context);
+ return account != null ? account.getBillingCycleDayLocal() : 0;
+ }
+
}
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 1ead0697bc..fc3fece0d7 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,9 @@
/*
* 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:
*
@@ -41,8 +43,6 @@
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.CustomField;
-import org.killbill.billing.util.customfield.StringCustomField;
import org.killbill.billing.util.customfield.dao.CustomFieldModelDao;
import org.killbill.billing.util.dao.TableName;
import org.killbill.billing.util.entity.Pagination;
@@ -76,6 +76,9 @@ public void testMinimalFields() throws Exception {
// Verify a default external key was set
Assert.assertEquals(retrievedAccount.getExternalKey(), retrievedAccount.getId().toString());
+
+ // Verify a default time zone was set
+ Assert.assertEquals(retrievedAccount.getTimeZone(), DateTimeZone.UTC);
}
@Test(groups = "slow", description = "Test Account: basic DAO calls")
@@ -169,8 +172,7 @@ public void testCustomFields() throws CustomFieldApiException {
final String fieldName = UUID.randomUUID().toString().substring(0, 4);
final String fieldValue = UUID.randomUUID().toString();
- final CustomField field = new StringCustomField(fieldName, fieldValue, ObjectType.ACCOUNT, accountId, internalCallContext.getCreatedDate());
- customFieldDao.create(new CustomFieldModelDao(field), internalCallContext);
+ customFieldDao.create(new CustomFieldModelDao(internalCallContext.getCreatedDate(), fieldName, fieldValue, accountId, ObjectType.ACCOUNT), internalCallContext);
final List customFieldMap = customFieldDao.getCustomFieldsForObject(accountId, ObjectType.ACCOUNT, internalCallContext);
Assert.assertEquals(customFieldMap.size(), 1);
diff --git a/api/pom.xml b/api/pom.xml
index f26b6fc1bf..e28d1d9257 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -19,7 +19,7 @@
killbill
org.kill-bill.billing
- 0.15.3-SNAPSHOT
+ 0.15.7-SNAPSHOT
../pom.xml
killbill-internal-api
diff --git a/api/src/main/java/org/killbill/billing/account/api/AccountInternalApi.java b/api/src/main/java/org/killbill/billing/account/api/AccountInternalApi.java
index 8e6ab3c472..4f7a92fea6 100644
--- a/api/src/main/java/org/killbill/billing/account/api/AccountInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/account/api/AccountInternalApi.java
@@ -21,23 +21,29 @@
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
-import org.killbill.billing.util.entity.Pagination;
public interface AccountInternalApi {
- public Account getAccountByKey(String key, InternalTenantContext context) throws AccountApiException;
+ Account getAccountByKey(String key, InternalTenantContext context) throws AccountApiException;
- public Account getAccountById(UUID accountId, InternalTenantContext context) throws AccountApiException;
+ Account getAccountById(UUID accountId, InternalTenantContext context) throws AccountApiException;
- public Account getAccountByRecordId(Long recordId, InternalTenantContext context) throws AccountApiException;
+ Account getAccountByRecordId(Long recordId, InternalTenantContext context) throws AccountApiException;
- public void updateAccount(String key, AccountData accountData, InternalCallContext context) throws AccountApiException;
+ void updateBCD(String key, int bcd, InternalCallContext context) throws AccountApiException;
- public List getEmails(UUID accountId, InternalTenantContext context);
+ int getBCD(UUID accountId, InternalTenantContext context) throws AccountApiException;
- public void removePaymentMethod(UUID accountId, InternalCallContext context) throws AccountApiException;
+ List getEmails(UUID accountId, InternalTenantContext context);
- public void updatePaymentMethod(UUID accountId, UUID paymentMethodId, InternalCallContext context) throws AccountApiException;
+ void removePaymentMethod(UUID accountId, InternalCallContext context) throws AccountApiException;
+
+ void updatePaymentMethod(UUID accountId, UUID paymentMethodId, InternalCallContext context) throws AccountApiException;
+
+ UUID getByRecordId(Long recordId, InternalTenantContext context) throws AccountApiException;
+
+ ImmutableAccountData getImmutableAccountDataById(UUID accountId, InternalTenantContext context) throws AccountApiException;
+
+ ImmutableAccountData getImmutableAccountDataByRecordId(Long recordId, InternalTenantContext context) throws AccountApiException;
- public UUID getByRecordId(Long recordId, InternalTenantContext context) throws AccountApiException;
}
diff --git a/api/src/main/java/org/killbill/billing/entitlement/AccountEntitlements.java b/api/src/main/java/org/killbill/billing/entitlement/AccountEntitlements.java
index df3fd30787..113605816d 100644
--- a/api/src/main/java/org/killbill/billing/entitlement/AccountEntitlements.java
+++ b/api/src/main/java/org/killbill/billing/entitlement/AccountEntitlements.java
@@ -20,14 +20,14 @@
import java.util.Map;
import java.util.UUID;
-import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.entitlement.api.Entitlement;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
// Wrapper object to save on DAO calls
public interface AccountEntitlements {
- public Account getAccount();
+ public ImmutableAccountData getAccount();
// Map bundle id -> bundle
public Map getBundles();
diff --git a/api/src/main/java/org/killbill/billing/entitlement/AccountEventsStreams.java b/api/src/main/java/org/killbill/billing/entitlement/AccountEventsStreams.java
index f7b558acea..bd674fb076 100644
--- a/api/src/main/java/org/killbill/billing/entitlement/AccountEventsStreams.java
+++ b/api/src/main/java/org/killbill/billing/entitlement/AccountEventsStreams.java
@@ -20,13 +20,13 @@
import java.util.Map;
import java.util.UUID;
-import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
// Wrapper object to save on DAO calls
public interface AccountEventsStreams {
- public Account getAccount();
+ public ImmutableAccountData getAccount();
// Map bundle id -> bundle
public Map getBundles();
diff --git a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
index c54ef4716b..0403e60412 100644
--- a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
@@ -37,7 +37,7 @@ public interface InvoiceInternalApi {
public BigDecimal getAccountBalance(UUID accountId, InternalTenantContext context);
- public void notifyOfPayment(UUID invoiceId, BigDecimal amountOutstanding, Currency currency, Currency processedCurrency, UUID paymentId, DateTime paymentDate, InternalCallContext context) throws InvoiceApiException;
+ public void notifyOfPayment(UUID invoiceId, BigDecimal amountOutstanding, Currency currency, Currency processedCurrency, UUID paymentId, DateTime paymentDate, boolean success, InternalCallContext context) throws InvoiceApiException;
public void notifyOfPayment(InvoicePayment invoicePayment, InternalCallContext context) throws InvoiceApiException;
diff --git a/api/src/main/java/org/killbill/billing/payment/plugin/api/NoOpPaymentPluginApi.java b/api/src/main/java/org/killbill/billing/jaxrs/JaxrsService.java
similarity index 55%
rename from api/src/main/java/org/killbill/billing/payment/plugin/api/NoOpPaymentPluginApi.java
rename to api/src/main/java/org/killbill/billing/jaxrs/JaxrsService.java
index bab9f06fb7..6d4ae5580c 100644
--- a/api/src/main/java/org/killbill/billing/payment/plugin/api/NoOpPaymentPluginApi.java
+++ b/api/src/main/java/org/killbill/billing/jaxrs/JaxrsService.java
@@ -1,7 +1,8 @@
/*
- * 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:
*
@@ -14,15 +15,9 @@
* under the License.
*/
-package org.killbill.billing.payment.plugin.api;
+package org.killbill.billing.jaxrs;
-public interface NoOpPaymentPluginApi extends PaymentPluginApi {
+import org.killbill.billing.platform.api.KillbillService;
- public void clear();
-
- public void makeNextPaymentFailWithError();
-
- public void makeNextPaymentFailWithException();
-
- public void makeAllInvoicesFailWithError(boolean failure);
+public interface JaxrsService extends KillbillService {
}
diff --git a/api/src/main/java/org/killbill/billing/junction/BillingEvent.java b/api/src/main/java/org/killbill/billing/junction/BillingEvent.java
index cfc0ea4292..a08e8c321e 100644
--- a/api/src/main/java/org/killbill/billing/junction/BillingEvent.java
+++ b/api/src/main/java/org/killbill/billing/junction/BillingEvent.java
@@ -34,86 +34,77 @@
public interface BillingEvent extends Comparable {
- /**
- * @return the account that this billing event is associated with
- */
- public Account getAccount();
/**
* @return the billCycleDay in the account timezone as seen for that subscription at that time
*
* Note: The billCycleDay may come from the Account, or the bundle or the subscription itself
*/
- public int getBillCycleDayLocal();
+ int getBillCycleDayLocal();
/**
* @return the subscription
*/
- public SubscriptionBase getSubscription();
+ SubscriptionBase getSubscription();
/**
* @return the date for when that event became effective
*/
- public DateTime getEffectiveDate();
+ DateTime getEffectiveDate();
/**
* @return the plan phase
*/
- public PlanPhase getPlanPhase();
+ PlanPhase getPlanPhase();
/**
* @return the plan
*/
- public Plan getPlan();
+ Plan getPlan();
/**
* @return the billing period for the active phase
*/
- public BillingPeriod getBillingPeriod();
-
- /**
- * @return the billing mode for the current event
- */
- public BillingMode getBillingMode();
+ BillingPeriod getBillingPeriod();
/**
* @return the description of the billing event
*/
- public String getDescription();
+ String getDescription();
/**
* @return the fixed price for the phase
*/
- public BigDecimal getFixedPrice();
+ BigDecimal getFixedPrice();
/**
* @return the recurring price for the phase
*/
- public BigDecimal getRecurringPrice();
+ BigDecimal getRecurringPrice();
/**
* @return the currency for the account being invoiced
*/
- public Currency getCurrency();
+ Currency getCurrency();
/**
* @return the transition type of the underlying subscription event that triggered this
*/
- public SubscriptionBaseTransitionType getTransitionType();
+ SubscriptionBaseTransitionType getTransitionType();
/**
* @return a unique long indicating the ordering on which events got inserted on disk-- used for sorting only
*/
- public Long getTotalOrdering();
+ Long getTotalOrdering();
/**
* @return the TimeZone of the account
*/
- public DateTimeZone getTimeZone();
+ DateTimeZone getTimeZone();
/**
*
* @return the list of {@code Usage} section
*/
- public List getUsages();
+ List getUsages();
}
diff --git a/api/src/main/java/org/killbill/billing/overdue/OverdueInternalApi.java b/api/src/main/java/org/killbill/billing/overdue/OverdueInternalApi.java
index afc01ca570..0b376b85fc 100644
--- a/api/src/main/java/org/killbill/billing/overdue/OverdueInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/overdue/OverdueInternalApi.java
@@ -16,7 +16,7 @@
package org.killbill.billing.overdue;
-import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.overdue.api.OverdueApiException;
import org.killbill.billing.overdue.api.OverdueState;
import org.killbill.billing.overdue.config.api.BillingState;
@@ -26,12 +26,12 @@
public interface OverdueInternalApi {
- public OverdueState refreshOverdueStateFor(Account overdueable, CallContext context) throws OverdueException, OverdueApiException;
+ public OverdueState refreshOverdueStateFor(ImmutableAccountData overdueable, CallContext context) throws OverdueException, OverdueApiException;
- public void setOverrideBillingStateForAccount(Account overdueable, BillingState state, CallContext context) throws OverdueException;
+ public void setOverrideBillingStateForAccount(ImmutableAccountData overdueable, BillingState state, CallContext context) throws OverdueException;
- public OverdueState getOverdueStateFor(Account overdueable, TenantContext context) throws OverdueException;
+ public OverdueState getOverdueStateFor(ImmutableAccountData overdueable, TenantContext context) throws OverdueException;
- public BillingState getBillingStateFor(Account overdueable, TenantContext context) throws OverdueException;
+ public BillingState getBillingStateFor(ImmutableAccountData overdueable, TenantContext context) throws OverdueException;
}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java
index 855e0a1971..a0a981f38a 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java
@@ -30,20 +30,9 @@
/**
* The interface {@code} shows a view of all the events for a particular {@code SubscriptionBase}.
*
- * It can be used to display information, or it can be used to modify the subscription stream of events
- * and 'repair' the stream by versioning the events.
*/
public interface SubscriptionBaseTimeline extends Entity {
- /**
- * @return the list of events that should be deleted when repairing the stream.
- */
- public List getDeletedEvents();
-
- /**
- * @return the list of events that should be added when repairing the stream
- */
- public List getNewEvents();
/**
* @return the current list of events for that {@code SubscriptionBase}
@@ -56,17 +45,16 @@ public interface SubscriptionBaseTimeline extends Entity {
public long getActiveVersion();
- public interface DeletedEvent {
+
+ public interface ExistingEvent {
/**
* @return the unique if for the event to delete
*/
public UUID getEventId();
- }
-
- public interface NewEvent {
/**
+ *
* @return the description for the event to be added
*/
public PlanPhaseSpecifier getPlanPhaseSpecifier();
@@ -81,10 +69,6 @@ public interface NewEvent {
*/
public SubscriptionBaseTransitionType getSubscriptionTransitionType();
- }
-
- public interface ExistingEvent extends DeletedEvent, NewEvent {
-
/**
* @return the date at which this event was effective
*/
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimelineApi.java b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimelineApi.java
index ae0a8617ea..1d96853cf5 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimelineApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimelineApi.java
@@ -16,23 +16,11 @@
package org.killbill.billing.subscription.api.timeline;
-import java.util.UUID;
-
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
-import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.TenantContext;
public interface SubscriptionBaseTimelineApi {
public BundleBaseTimeline getBundleTimeline(SubscriptionBaseBundle bundle, TenantContext context)
throws SubscriptionBaseRepairException;
-
- public BundleBaseTimeline getBundleTimeline(UUID accountId, String bundleName, TenantContext context)
- throws SubscriptionBaseRepairException;
-
- public BundleBaseTimeline getBundleTimeline(UUID bundleId, TenantContext context)
- throws SubscriptionBaseRepairException;
-
- public BundleBaseTimeline repairBundle(BundleBaseTimeline input, boolean dryRun, CallContext context)
- throws SubscriptionBaseRepairException;
}
diff --git a/api/src/main/java/org/killbill/billing/util/UUIDs.java b/api/src/main/java/org/killbill/billing/util/UUIDs.java
index 698afd7c52..9a89bddb06 100644
--- a/api/src/main/java/org/killbill/billing/util/UUIDs.java
+++ b/api/src/main/java/org/killbill/billing/util/UUIDs.java
@@ -149,7 +149,7 @@ private static DigestRandomGenerator sha1Generator() {
return new DigestRandomGenerator(MessageDigest.getInstance("SHA-1"));
}
catch (NoSuchAlgorithmException ex) {
- throw new AssertionError("unexpeced missing SHA-1 digest", ex);
+ throw new Error("unexpeced missing SHA-1 digest", ex);
}
}
diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index 8613028dba..8a619f0df1 100644
--- a/beatrix/pom.xml
+++ b/beatrix/pom.xml
@@ -19,7 +19,7 @@
killbill
org.kill-bill.billing
- 0.15.3-SNAPSHOT
+ 0.15.7-SNAPSHOT
../pom.xml
killbill-beatrix
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
index 442f5f4c73..98c0ce59ae 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
@@ -40,6 +40,7 @@
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.api.SubscriptionBundle;
import org.killbill.billing.entitlement.api.SubscriptionEventType;
+import org.killbill.billing.invoice.api.DryRunType;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItemType;
@@ -75,7 +76,7 @@ public void testCancelBPWithAOTheSameDay() throws Exception {
// CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
//
- TestDryRunArguments dryRun = new TestDryRunArguments("Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, null, null,
+ TestDryRunArguments dryRun = new TestDryRunArguments(DryRunType.SUBSCRIPTION_ACTION, "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, null, null,
SubscriptionEventType.START_BILLING, null, null, clock.getUTCNow(), null);
Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), dryRun, callContext);
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
@@ -90,7 +91,7 @@ public void testCancelBPWithAOTheSameDay() throws Exception {
//
// ADD ADD_ON ON THE SAME DAY
//
- dryRun = new TestDryRunArguments("Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, null, null,
+ dryRun = new TestDryRunArguments(DryRunType.SUBSCRIPTION_ACTION, "Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, null, null,
SubscriptionEventType.START_BILLING, null, bpSubscription.getBundleId(), clock.getUTCNow(), null);
dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), dryRun, callContext);
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("399.95")));
@@ -106,7 +107,7 @@ public void testCancelBPWithAOTheSameDay() throws Exception {
// CANCEL BP ON THE SAME DAY (we should have two cancellations, BP and AO)
// There is no invoice created as we only adjust the previous invoice.
//
- dryRun = new TestDryRunArguments(null, null, null, null, null, SubscriptionEventType.STOP_BILLING, bpSubscription.getId(),
+ dryRun = new TestDryRunArguments(DryRunType.SUBSCRIPTION_ACTION, null, null, null, null, null, SubscriptionEventType.STOP_BILLING, bpSubscription.getId(),
bpSubscription.getBundleId(), clock.getUTCNow(), null);
dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), dryRun, callContext);
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-399.95")));
@@ -153,7 +154,7 @@ public void testBasePlanCompleteWithBillingDayInPast() throws Exception {
//
// CHANGE PLAN IMMEDIATELY AND EXPECT BOTH EVENTS: NextEvent.CHANGE NextEvent.INVOICE
//
- TestDryRunArguments dryRun = new TestDryRunArguments("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, null, null, SubscriptionEventType.CHANGE,
+ TestDryRunArguments dryRun = new TestDryRunArguments(DryRunType.SUBSCRIPTION_ACTION, "Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, null, null, SubscriptionEventType.CHANGE,
subscription.getId(), subscription.getBundleId(), clock.getUTCNow(), null);
Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), dryRun, callContext);
expectedInvoices.add(new ExpectedInvoiceItemCheck(initialCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
@@ -173,7 +174,7 @@ public void testBasePlanCompleteWithBillingDayInPast() throws Exception {
setDateAndCheckForCompletion(new DateTime(2012, 3, 1, 23, 59, 59, 0, testTimeZone));
DateTime nextDate = clock.getUTCNow().plusDays(1);
- dryRun = new TestDryRunArguments();
+ dryRun = new TestDryRunArguments(DryRunType.TARGET_DATE);
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2012, 3, 2), new LocalDate(2012, 3, 31), InvoiceItemType.RECURRING, new BigDecimal("561.24")));
@@ -202,7 +203,7 @@ public void testBasePlanCompleteWithBillingDayInPast() throws Exception {
nextDate = clock.getUTCNow().plusDays(31);
- dryRun = new TestDryRunArguments();
+ dryRun = new TestDryRunArguments(DryRunType.TARGET_DATE);
dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), new LocalDate(nextDate, testTimeZone), dryRun, callContext);
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2012, 3, 31), new LocalDate(2012, 4, 30), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
@@ -287,7 +288,7 @@ public void testBasePlanCompleteWithBillingDayAlignedWithTrial() throws Exceptio
//
- TestDryRunArguments dryRun = new TestDryRunArguments("Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, null, null, SubscriptionEventType.CHANGE,
+ TestDryRunArguments dryRun = new TestDryRunArguments(DryRunType.SUBSCRIPTION_ACTION, "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, null, null, SubscriptionEventType.CHANGE,
subscription.getId(), subscription.getBundleId(), null, null);
try {
invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), dryRun, callContext);
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index a08193912a..3a17e84add 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -61,6 +61,7 @@
import org.killbill.billing.entitlement.api.SubscriptionApi;
import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.invoice.api.DryRunArguments;
+import org.killbill.billing.invoice.api.DryRunType;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoicePaymentApi;
@@ -243,8 +244,6 @@ public List getPaymentControlPluginNames() {
@Inject
protected IDBI idbi;
- @Inject
- protected CacheControllerDispatcher controlCacheDispatcher;
@Inject
protected TestApiListener busHandler;
@@ -273,9 +272,6 @@ public void beforeMethod() throws Exception {
//Thread.currentThread().setContextClassLoader(null);
log.debug("RESET TEST FRAMEWORK");
-
- controlCacheDispatcher.clearAll();
-
overdueConfigCache.loadDefaultOverdueConfig((OverdueConfig) null);
clock.resetDeltaFromReality();
@@ -715,6 +711,7 @@ private T doCallAndCheckForCompletion(final Function f, final NextE
protected static class TestDryRunArguments implements DryRunArguments {
+ private final DryRunType dryRunType;
private final PlanPhaseSpecifier spec;
private final SubscriptionEventType action;
private final UUID subscriptionId;
@@ -722,7 +719,8 @@ protected static class TestDryRunArguments implements DryRunArguments {
private final DateTime effectiveDate;
private final BillingActionPolicy billingPolicy;
- public TestDryRunArguments() {
+ public TestDryRunArguments(final DryRunType dryRunType) {
+ this.dryRunType = dryRunType;
this.spec = null;
this.action = null;
this.subscriptionId = null;
@@ -731,7 +729,8 @@ public TestDryRunArguments() {
this.billingPolicy = null;
}
- public TestDryRunArguments(final String productName,
+ public TestDryRunArguments(final DryRunType dryRunType,
+ final String productName,
final ProductCategory category,
final BillingPeriod billingPeriod,
final String priceList,
@@ -741,6 +740,7 @@ public TestDryRunArguments(final String productName,
final UUID bundleId,
final DateTime effectiveDate,
final BillingActionPolicy billingPolicy) {
+ this.dryRunType = dryRunType;
this.spec = new PlanPhaseSpecifier(productName, category, billingPeriod, priceList, phaseType);
this.action = action;
this.subscriptionId = subscriptionId;
@@ -749,6 +749,11 @@ public TestDryRunArguments(final String productName,
this.billingPolicy = billingPolicy;
}
+ @Override
+ public DryRunType getDryRunType() {
+ return dryRunType;
+ }
+
@Override
public PlanPhaseSpecifier getPlanPhaseSpecifier() {
return spec;
@@ -780,7 +785,7 @@ public BillingActionPolicy getBillingActionPolicy() {
}
@Override
- public List getPlanPhasePriceoverrides() {
+ public List getPlanPhasePriceOverrides() {
return null;
}
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
index a7a0b4c5a9..3d81d737c4 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
@@ -19,10 +19,13 @@
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
+import java.util.UUID;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
+import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
@@ -30,11 +33,21 @@
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.invoice.api.DryRunArguments;
+import org.killbill.billing.invoice.api.DryRunType;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.testng.Assert;
import org.testng.annotations.Test;
+import com.google.common.collect.ImmutableList;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
public class TestIntegrationInvoice extends TestIntegrationBase {
//
@@ -66,7 +79,7 @@ public void testDryRunWithNoTargetDate() throws Exception {
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 6, 14), new LocalDate(2015, 7, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
// This will verify that the upcoming Phase is found and the invoice is generated at the right date, with correct items
- DryRunArguments dryRun = new TestDryRunArguments();
+ DryRunArguments dryRun = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE);
Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
@@ -132,12 +145,12 @@ public void testDryRunWithNoTargetDateAndMultipleNonAlignedSubscriptions() throw
final List expectedInvoices = new ArrayList();
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2014, 2, 1), new LocalDate(2015, 2, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
- DryRunArguments dryRun = new TestDryRunArguments();
+ DryRunArguments dryRun = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE);
Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
- // 2014-1-2
+ // 2014-2-1
clock.addDays(30);
assertListenerStatus();
invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, expectedInvoices);
@@ -172,7 +185,16 @@ public void testDryRunWithNoTargetDateAndMultipleNonAlignedSubscriptions() throw
invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, expectedInvoices);
expectedInvoices.clear();
- //
+
+ // We test first the next expected invoice for a specific subscription: We can see the targetDate is 2015-2-14 and not 2015-2-1
+ final DryRunArguments dryRunWIthSubscription = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, subscriptionMonthly.getId(), null, null, null);
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRunWIthSubscription, callContext);
+ assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 14));
+ invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+ expectedInvoices.clear();
+
+ // Then we test first the next expected invoice at the account level
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 1), new LocalDate(2016, 2, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
@@ -187,8 +209,66 @@ public void testDryRunWithNoTargetDateAndMultipleNonAlignedSubscriptions() throw
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, expectedInvoices);
+ }
+
+
+ @Test(groups = "slow")
+ public void testApplyCreditOnExistingBalance() throws Exception {
+
+
+ final int billingDay = 14;
+ final DateTime initialCreationDate = new DateTime(2015, 5, 15, 0, 0, 0, 0, testTimeZone);
+
+ log.info("Beginning test with BCD of " + billingDay);
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+
+ add_AUTO_PAY_OFF_Tag(account.getId(), ObjectType.ACCOUNT);
+
+ // set clock to the initial start date
+ clock.setTime(initialCreationDate);
+
+ createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+
+ // Move through time and verify we get the same invoice
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ final Collection invoices = invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), new LocalDate(clock.getUTCNow(), account.getTimeZone()), callContext);
+ assertEquals(invoices.size(), 1);
+
+ final UUID unpaidInvoiceId = invoices.iterator().next().getId();
+ final Invoice unpaidInvoice = invoiceUserApi.getInvoice(unpaidInvoiceId, callContext);
+ assertTrue(unpaidInvoice.getBalance().compareTo(new BigDecimal("249.95")) == 0);
+
+ final BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance1.compareTo(new BigDecimal("249.95")) == 0);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE_ADJUSTMENT);
+ invoiceUserApi.insertCredit(account.getId(), new BigDecimal("300"), new LocalDate(clock.getUTCNow(), account.getTimeZone()), account.getCurrency(), callContext);
+ assertListenerStatus();
+
+ final BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance2.compareTo(new BigDecimal("-50.05")) == 0);
+
+ final Invoice unpaidInvoice2 = invoiceUserApi.getInvoice(unpaidInvoiceId, callContext);
+ assertTrue(unpaidInvoice2.getBalance().compareTo(BigDecimal.ZERO) == 0);
+
+ remove_AUTO_PAY_OFF_Tag(account.getId(), ObjectType.ACCOUNT);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT);
+ clock.addDays(31);
+ assertListenerStatus();
+
+ final BigDecimal accountBalance3 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance3.compareTo(BigDecimal.ZERO) == 0);
+
+ final List payments = paymentApi.getAccountPayments(account.getId(), false, ImmutableList.of(), callContext);
+ assertEquals(payments.size(), 1);
+ final Payment payment = payments.get(0);
+ assertTrue(payment.getPurchasedAmount().compareTo(new BigDecimal("199.90")) == 0);
}
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
index e68aab1374..13a3a43046 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
@@ -18,21 +18,29 @@
package org.killbill.billing.beatrix.integration;
import java.math.BigDecimal;
+import java.util.List;
import org.joda.time.LocalDate;
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountData;
import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PluginProperty;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
public class TestInvoicePayment extends TestIntegrationBase {
@@ -97,4 +105,58 @@ public void testPartialPayments() throws Exception {
assertTrue(accountBalance.compareTo(new BigDecimal("4.00")) == 0);
}
+
+ //
+
+ @Test(groups = "slow")
+ public void testWithPaymentFailure() throws Exception {
+
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ paymentPlugin.makeNextPaymentFailWithError();
+
+ createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ final List invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 2);
+
+ final Invoice invoice1 = invoices.get(0).getInvoiceItems().get(0).getInvoiceItemType() == InvoiceItemType.RECURRING ?
+ invoices.get(0) : invoices.get(1);
+ assertTrue(invoice1.getBalance().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice1.getPaidAmount().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice1.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice1.getPayments().size(), 1);
+ assertFalse(invoice1.getPayments().get(0).isSuccess());
+
+ BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance1.compareTo(new BigDecimal("249.95")) == 0);
+
+ final List payments = paymentApi.getAccountPayments(account.getId(), false, ImmutableList.of(), callContext);
+ assertEquals(payments.size(), 1);
+
+ // Trigger the payment retry
+ busHandler.pushExpectedEvents(NextEvent.PAYMENT);
+ clock.addDays(8);
+ assertListenerStatus();
+
+ Invoice invoice2 = invoiceUserApi.getInvoice(invoice1.getId(), callContext);
+ assertTrue(invoice2.getBalance().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice2.getPaidAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice2.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice2.getPayments().size(), 1);
+ assertTrue(invoice2.getPayments().get(0).isSuccess());
+
+ BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance2.compareTo(BigDecimal.ZERO) == 0);
+ }
+
+
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPublicBus.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPublicBus.java
index 012fa68b27..6bde96a568 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPublicBus.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPublicBus.java
@@ -85,8 +85,6 @@ public void beforeMethod() throws Exception {
log.debug("RESET TEST FRAMEWORK");
- controlCacheDispatcher.clearAll();
-
overdueConfigCache.loadDefaultOverdueConfig((OverdueConfig) null);
clock.resetDeltaFromReality();
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestRepairIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestRepairIntegration.java
deleted file mode 100644
index 5c9b10410a..0000000000
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestRepairIntegration.java
+++ /dev/null
@@ -1,360 +0,0 @@
-/*
- * Copyright 2010-2013 Ning, Inc.
- * 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.beatrix.integration;
-
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.UUID;
-
-import org.joda.time.DateTime;
-import org.joda.time.Interval;
-import org.testng.Assert;
-import org.testng.annotations.Test;
-
-import org.killbill.billing.account.api.Account;
-import org.killbill.billing.api.TestApiListener.NextEvent;
-import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.catalog.api.PhaseType;
-import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
-import org.killbill.billing.catalog.api.PriceListSet;
-import org.killbill.billing.catalog.api.ProductCategory;
-import org.killbill.billing.entitlement.api.DefaultEntitlement;
-import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
-import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
-import org.killbill.billing.subscription.api.timeline.BundleBaseTimeline;
-import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline;
-import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.DeletedEvent;
-import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.ExistingEvent;
-import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.NewEvent;
-import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
-import org.killbill.billing.subscription.api.user.SubscriptionEvents;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
-
-public class TestRepairIntegration extends TestIntegrationBase {
-
- @Test(groups = "slow", enabled = false)
- public void testRepairChangeBPWithAddonIncludedIntrial() throws Exception {
- log.info("Starting testRepairChangeBPWithAddonIncludedIntrial");
- testRepairChangeBPWithAddonIncluded(true);
- }
-
- @Test(groups = "slow", enabled = false)
- public void testRepairChangeBPWithAddonIncludedOutOfTrial() throws Exception {
- log.info("Starting testRepairChangeBPWithAddonIncludedOutOfTrial");
- testRepairChangeBPWithAddonIncluded(false);
- }
-
- private void testRepairChangeBPWithAddonIncluded(final boolean inTrial) throws Exception {
-
- final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
- clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
-
- final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(25));
-
- final String productName = "Shotgun";
- final BillingPeriod term = BillingPeriod.MONTHLY;
- final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
-
- final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
- assertNotNull(bpEntitlement);
-
- // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL
- Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3));
- clock.addDeltaFromReality(it.toDurationMillis());
-
- final DefaultEntitlement aoEntitlement1 = addAOEntitlementAndCheckForCompletion(bpEntitlement.getBundleId(), "Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE, NextEvent.PAYMENT);
- final DefaultEntitlement aoEntitlement2 = addAOEntitlementAndCheckForCompletion(bpEntitlement.getBundleId(), "Laser-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE, NextEvent.PAYMENT);
-
- // MOVE CLOCK A LITTLE BIT MORE -- EITHER STAY IN TRIAL OR GET OUT
- final int duration = inTrial ? 3 : 35;
- it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(duration));
- if (!inTrial) {
- busHandler.pushExpectedEvent(NextEvent.PHASE);
- busHandler.pushExpectedEvent(NextEvent.PHASE);
- busHandler.pushExpectedEvent(NextEvent.PHASE);
- busHandler.pushExpectedEvent(NextEvent.INVOICE);
- busHandler.pushExpectedEvent(NextEvent.PAYMENT);
- }
- clock.addDeltaFromReality(it.toDurationMillis());
- if (!inTrial) {
- assertListenerStatus();
- }
- final boolean ifRepair = false;
- if (ifRepair) {
- BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bpEntitlement.getSubscriptionBase().getBundleId(), callContext);
- sortEventsOnBundle(bundleRepair);
-
- // Quick check
- SubscriptionBaseTimeline bpRepair = getSubscriptionRepair(bpEntitlement.getId(), bundleRepair);
- assertEquals(bpRepair.getExistingEvents().size(), 2);
-
- final SubscriptionBaseTimeline aoRepair = getSubscriptionRepair(aoEntitlement1.getId(), bundleRepair);
- assertEquals(aoRepair.getExistingEvents().size(), 2);
-
- final SubscriptionBaseTimeline aoRepair2 = getSubscriptionRepair(aoEntitlement2.getId(), bundleRepair);
- assertEquals(aoRepair2.getExistingEvents().size(), 2);
-
- final DateTime bpChangeDate = clock.getUTCNow().minusDays(1);
-
- final List des = new LinkedList();
- des.add(createDeletedEvent(bpRepair.getExistingEvents().get(1).getEventId()));
-
- final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL);
- final NewEvent ne = createNewEvent(SubscriptionBaseTransitionType.CHANGE, bpChangeDate, spec);
-
- bpRepair = createSubscriptionReapir(bpEntitlement.getId(), des, Collections.singletonList(ne));
-
- bundleRepair = createBundleRepair(bpEntitlement.getSubscriptionBase().getBundleId(), bundleRepair.getViewId(), Collections.singletonList(bpRepair));
-
- // TIME TO REPAIR
- busHandler.pushExpectedEvent(NextEvent.INVOICE);
- busHandler.pushExpectedEvent(NextEvent.PAYMENT);
- busHandler.pushExpectedEvent(NextEvent.REPAIR_BUNDLE);
- repairApi.repairBundle(bundleRepair, false, callContext);
- assertListenerStatus();
-
- final DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) aoEntitlement1.getSubscriptionBase();
- assertEquals(newAoSubscription.getState(), EntitlementState.CANCELLED);
- assertEquals(newAoSubscription.getAllTransitions().size(), 2);
- assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
-
- final DefaultSubscriptionBase newAoSubscription2 = (DefaultSubscriptionBase) aoEntitlement2.getSubscriptionBase();
- assertEquals(newAoSubscription2.getState(), EntitlementState.ACTIVE);
- assertEquals(newAoSubscription2.getAllTransitions().size(), 2);
- assertEquals(newAoSubscription2.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
-
- final DefaultSubscriptionBase newBaseSubscription = (DefaultSubscriptionBase) bpEntitlement.getSubscriptionBase();
- assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE);
- assertEquals(newBaseSubscription.getAllTransitions().size(), 3);
- assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
-
- assertListenerStatus();
- }
-
- checkNoMoreInvoiceToGenerate(account);
- }
-
- protected SubscriptionBaseTimeline createSubscriptionReapir(final UUID id, final List deletedEvents, final List newEvents) {
- return new SubscriptionBaseTimeline() {
- @Override
- public UUID getId() {
- return id;
- }
-
- @Override
- public DateTime getCreatedDate() {
- return null;
- }
-
- @Override
- public DateTime getUpdatedDate() {
- return null;
- }
-
- @Override
- public List getNewEvents() {
- return newEvents;
- }
-
- @Override
- public List getExistingEvents() {
- return null;
- }
-
- @Override
- public List getDeletedEvents() {
- return deletedEvents;
- }
-
- @Override
- public long getActiveVersion() {
- return 0;
- }
- };
- }
-
- protected BundleBaseTimeline createBundleRepair(final UUID bundleId, final String viewId, final List subscriptionRepair) {
- return new BundleBaseTimeline() {
- @Override
- public String getViewId() {
- return viewId;
- }
-
- @Override
- public List getSubscriptions() {
- return subscriptionRepair;
- }
-
- @Override
- public UUID getId() {
- return bundleId;
- }
-
- @Override
- public DateTime getCreatedDate() {
- return null;
- }
-
- @Override
- public DateTime getUpdatedDate() {
- return null;
- }
-
- @Override
- public String getExternalKey() {
- return null;
- }
- };
- }
-
- protected ExistingEvent createExistingEventForAssertion(final SubscriptionBaseTransitionType type,
- final String productName, final PhaseType phaseType, final ProductCategory category, final String priceListName, final BillingPeriod billingPeriod,
- final DateTime effectiveDateTime) {
-
- final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(productName, category, billingPeriod, priceListName, phaseType);
- final ExistingEvent ev = new ExistingEvent() {
- @Override
- public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
- return type;
- }
-
- @Override
- public DateTime getRequestedDate() {
- return null;
- }
-
- @Override
- public PlanPhaseSpecifier getPlanPhaseSpecifier() {
- return spec;
- }
-
- @Override
- public UUID getEventId() {
- return null;
- }
-
- @Override
- public DateTime getEffectiveDate() {
- return effectiveDateTime;
- }
-
- @Override
- public String getPlanName() {
- return null;
- }
-
- @Override
- public String getPlanPhaseName() {
- return null;
- }
- };
- return ev;
- }
-
- protected SubscriptionBaseTimeline getSubscriptionRepair(final UUID id, final BundleBaseTimeline bundleRepair) {
- for (final SubscriptionBaseTimeline cur : bundleRepair.getSubscriptions()) {
- if (cur.getId().equals(id)) {
- return cur;
- }
- }
- Assert.fail("Failed to find SubscriptionReapir " + id);
- return null;
- }
-
- protected void validateExistingEventForAssertion(final ExistingEvent expected, final ExistingEvent input) {
-
- log.info(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getProductName(), expected.getPlanPhaseSpecifier().getProductName()));
- assertEquals(input.getPlanPhaseSpecifier().getProductName(), expected.getPlanPhaseSpecifier().getProductName());
- log.info(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getPhaseType(), expected.getPlanPhaseSpecifier().getPhaseType()));
- assertEquals(input.getPlanPhaseSpecifier().getPhaseType(), expected.getPlanPhaseSpecifier().getPhaseType());
- log.info(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getProductCategory(), expected.getPlanPhaseSpecifier().getProductCategory()));
- assertEquals(input.getPlanPhaseSpecifier().getProductCategory(), expected.getPlanPhaseSpecifier().getProductCategory());
- log.info(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getPriceListName(), expected.getPlanPhaseSpecifier().getPriceListName()));
- assertEquals(input.getPlanPhaseSpecifier().getPriceListName(), expected.getPlanPhaseSpecifier().getPriceListName());
- log.info(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getBillingPeriod(), expected.getPlanPhaseSpecifier().getBillingPeriod()));
- assertEquals(input.getPlanPhaseSpecifier().getBillingPeriod(), expected.getPlanPhaseSpecifier().getBillingPeriod());
- log.info(String.format("Got %s -> Expected %s", input.getEffectiveDate(), expected.getEffectiveDate()));
- assertEquals(input.getEffectiveDate(), expected.getEffectiveDate());
- }
-
- protected DeletedEvent createDeletedEvent(final UUID eventId) {
- return new DeletedEvent() {
- @Override
- public UUID getEventId() {
- return eventId;
- }
- };
- }
-
- protected NewEvent createNewEvent(final SubscriptionBaseTransitionType type, final DateTime requestedDate, final PlanPhaseSpecifier spec) {
-
- return new NewEvent() {
- @Override
- public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
- return type;
- }
-
- @Override
- public DateTime getRequestedDate() {
- return requestedDate;
- }
-
- @Override
- public PlanPhaseSpecifier getPlanPhaseSpecifier() {
- return spec;
- }
- };
- }
-
- protected void sortEventsOnBundle(final BundleBaseTimeline bundle) {
- if (bundle.getSubscriptions() == null) {
- return;
- }
- for (final SubscriptionBaseTimeline cur : bundle.getSubscriptions()) {
- if (cur.getExistingEvents() != null) {
- sortExistingEvent(cur.getExistingEvents());
- }
- if (cur.getNewEvents() != null) {
- sortNewEvent(cur.getNewEvents());
- }
- }
- }
-
- protected void sortExistingEvent(final List events) {
- Collections.sort(events, new Comparator() {
- @Override
- public int compare(final ExistingEvent arg0, final ExistingEvent arg1) {
- return arg0.getEffectiveDate().compareTo(arg1.getEffectiveDate());
- }
- });
- }
-
- protected void sortNewEvent(final List events) {
- Collections.sort(events, new Comparator() {
- @Override
- public int compare(final NewEvent arg0, final NewEvent arg1) {
- return arg0.getRequestedDate().compareTo(arg1.getRequestedDate());
- }
- });
- }
-}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
index 3dc6b0eb15..0d21887eec 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
@@ -24,6 +24,7 @@
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.killbill.billing.entitlement.api.SubscriptionEventType;
+import org.killbill.billing.invoice.api.DryRunType;
import org.testng.annotations.Test;
import org.killbill.billing.account.api.Account;
@@ -87,7 +88,7 @@ public void testForcePolicy() throws Exception {
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2334.20")),
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("2164.88"), false /* Issue with test where created date for context is wrong*/));
- TestDryRunArguments dryRun = new TestDryRunArguments(productName, ProductCategory.BASE, BillingPeriod.MONTHLY, null, null,
+ TestDryRunArguments dryRun = new TestDryRunArguments(DryRunType.SUBSCRIPTION_ACTION, productName, ProductCategory.BASE, BillingPeriod.MONTHLY, null, null,
SubscriptionEventType.CHANGE, bpEntitlement.getId(), bpEntitlement.getBundleId(), null, BillingActionPolicy.IMMEDIATE);
Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), dryRun, callContext);
invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, callContext, toBeChecked);
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java
index f3a8a7a9b8..f71adbfbd7 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java
@@ -194,7 +194,7 @@ public TestInvoicePluginApi() {
}
@Override
- public List getAdditionalInvoiceItems(final Invoice invoice, final Iterable pluginProperties, final CallContext callContext) {
+ public List getAdditionalInvoiceItems(final Invoice invoice, final boolean isDryRun, final Iterable pluginProperties, final CallContext callContext) {
return addTaxItem.compareAndSet(true, false) ? ImmutableList.of(createTaxInvoiceItem(invoice)) : ImmutableList.of();
}
@@ -205,5 +205,6 @@ private InvoiceItem createTaxInvoiceItem(final Invoice invoice) {
public void addTaxItem() {
this.addTaxItem.set(true);
}
+
}
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java
index 1323c317ef..6713be18ce 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java
@@ -27,10 +27,12 @@
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.integration.TestIntegrationBase;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.usage.api.SubscriptionUsageRecord;
import org.killbill.billing.usage.api.UnitUsageRecord;
import org.killbill.billing.usage.api.UsageRecord;
@@ -38,6 +40,8 @@
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.google.common.collect.ImmutableList;
+
public class TestConsumableInArrear extends TestIntegrationBase {
@BeforeMethod(groups = "slow")
@@ -46,7 +50,7 @@ public void beforeMethod() throws Exception {
}
@Test(groups = "slow")
- public void testSimple() throws Exception {
+ public void testWithNoUsageInPeriodAndOldUsage() throws Exception {
final AccountData accountData = getAccountData(1);
final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
@@ -73,7 +77,7 @@ public void testSimple() throws Exception {
setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 15), 100L, callContext);
busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
- clock.setDay(new LocalDate(2012, 5, 1));
+ clock.addDays(30);
assertListenerStatus();
invoiceChecker.checkInvoice(account.getId(), 2, callContext,
@@ -81,20 +85,19 @@ public void testSimple() throws Exception {
new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), InvoiceItemType.USAGE, new BigDecimal("5.90")));
// We don't expect any invoice, but we want to give the system the time to verify there is nothing to do so we can fail
- clock.setDay(new LocalDate(2012, 6, 1));
+ clock.addMonths(1);
Thread.sleep(1000);
setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 6, 1), 50L, callContext);
setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 6, 16), 300L, callContext);
busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT);
- clock.setDay(new LocalDate(2012, 7, 1));
+ clock.addMonths(1);
assertListenerStatus();
invoiceChecker.checkInvoice(account.getId(), 3, callContext,
new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.USAGE, new BigDecimal("11.80")));
-
// Should be ignored because this is outside of optimization range (org.killbill.invoice.readMaxRawUsagePreviousPeriod = 2) => we will only look for items > 2012-7-1 - 2 months = 2012-5-1
setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 30), 100L, callContext);
@@ -106,12 +109,64 @@ public void testSimple() throws Exception {
setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 7, 16), 300L, callContext);
busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT);
- clock.setDay(new LocalDate(2012, 8, 1));
+ clock.addMonths(1);
assertListenerStatus();
invoiceChecker.checkInvoice(account.getId(), 4, callContext,
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.USAGE, new BigDecimal("5.90")),
new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 1), new LocalDate(2012, 8, 1), InvoiceItemType.USAGE, new BigDecimal("11.80")));
+ }
+
+ @Test(groups = "slow")
+ public void testWithCancellation() throws Exception {
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ // We take april as it has 30 days (easier to play with BCD)
+ // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ //
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+ //
+ final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.INVOICE);
+ // Check bundle after BP got created otherwise we get an error from auditApi.
+ subscriptionChecker.checkSubscriptionCreated(bpSubscription.getId(), internalCallContext);
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+
+ //
+ // ADD ADD_ON ON THE SAME DAY
+ //
+ final DefaultEntitlement aoSubscription = addAOEntitlementAndCheckForCompletion(bpSubscription.getBundleId(), "Bullets", ProductCategory.ADD_ON, BillingPeriod.NO_BILLING_PERIOD, NextEvent.CREATE);
+
+ setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 1), 99L, callContext);
+ setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 15), 100L, callContext);
+
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), InvoiceItemType.USAGE, new BigDecimal("5.90")));
+
+ setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 5, 3), 99L, callContext);
+ setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 5, 5), 100L, callContext);
+
+ // This one should be ignored
+ setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 5, 29), 100L, callContext);
+
+ clock.addDays(27);
+ busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.INVOICE, NextEvent.PAYMENT);
+ aoSubscription.cancelEntitlementWithDateOverrideBillingPolicy(new LocalDate(2012, 5, 28), BillingActionPolicy.IMMEDIATE, ImmutableList.of(), callContext);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 28), InvoiceItemType.USAGE, new BigDecimal("5.90")));
+
+ clock.addDays(4);
+ Thread.sleep(1000);
}
diff --git a/bin/db-helper b/bin/db-helper
index 929c59a8eb..c642b1c666 100755
--- a/bin/db-helper
+++ b/bin/db-helper
@@ -19,7 +19,7 @@
# #
###################################################################################
-#set -x
+# set -x
HERE=`cd \`dirname $0\`; pwd`
TOP=$HERE/..
@@ -27,27 +27,49 @@ TOP=$HERE/..
POM="$TOP/pom.xml"
ACTION=
+HOST="localhost"
DATABASE="killbill"
USER="root"
PWD="root"
+PORT=
+PORT_MYSQL=3306
+PORT_POSTGRES=5432
+DRIVER="mysql"
TEST_ALSO=
OUTPUT_FILE=
-
DDL_FILE=
CLEAN_FILE=
# Egrep like for skipping some modules until they are ready
SKIP="(server)"
+
+# test if user is running gnu-getopt
+TEST=`getopt -o "a:" -l "action:" -- --action dump`
+if [ "$TEST" != " --action 'dump' --" ]; then
+ echo "You are not using gnu-getopt or latest getopt."
+ echo "For Mac OS X, please upgrade 'getopt' to 'gnu-getopt',"
+ echo "For Linux, please upgrade 'getopt'."
+ exit
+fi
+
+
+ARGS=`getopt -o "a:d:h:u:p:t:f:" -l "action:,driver:,database:,host:,user:,password:,test:,file:,port:,help" -n "db-helper" -- "$@"`
+eval set -- "${ARGS}"
+
+
function usage() {
- echo -n "./db_helper "
- echo -n " -a "
- echo -n " -d database_name (default = killbill)"
- echo -n " -u user_name (default = root)"
- echo -n " -p password (default = root)"
- echo -n " -t (also include test ddl)"
- echo -n " -f file (output file, for dump only)"
- echo -n " -h this message"
+ echo -n "./db_helper"
+ echo -n " -a|--action "
+ echo -n " --driver (default = mysql)"
+ echo -n " -h|--host host (default = localhost)"
+ echo -n " --port port"
+ echo -n " -d|--database database_name (default = killbill)"
+ echo -n " -u|--user user_name (default = root)"
+ echo -n " -p|--password password (default = root)"
+ echo -n " -t|--test (also include test ddl)"
+ echo -n " -f|--file file (output file, for dump only)"
+ echo -n " --help this message"
echo
exit 1
}
@@ -67,10 +89,10 @@ function find_test_ddl() {
ddl_test="$ddl_test $cur_ddl"
done
echo "$ddl_test"
-
}
-function find_src_ddl() {
+
+function find_src_ddl() {
local modules=`get_modules`
local ddl_src=
@@ -88,7 +110,7 @@ function create_clean_file() {
local tables=`cat $ddl_file | grep -i "create table" | awk ' { print $3 } '`
local tmp="/tmp/clean-$DATABASE.$$"
- echo "/*! use $DATABASE; */" >> $tmp
+ echo "/*! USE $DATABASE */;" >> $tmp
echo "" >> $tmp
for t in $tables; do
echo "truncate $t;" >> $tmp
@@ -96,6 +118,7 @@ function create_clean_file() {
echo $tmp
}
+
function create_ddl_file() {
local ddls=`find_src_ddl`
local test_ddls=
@@ -106,7 +129,10 @@ function create_ddl_file() {
local tmp="/tmp/ddl-$DATABASE.$$"
touch $tmp
- echo "/*! use $DATABASE; */" >> $tmp
+ if [ $DRIVER == "postgres" ]; then
+ cat util/src/main/resources/org/killbill/billing/util/ddl-postgresql.sql > $tmp
+ fi
+ echo "/*! USE $DATABASE */;" >> $tmp
echo "" >> $tmp
for d in $ddls; do
cat $d >> $tmp
@@ -115,32 +141,62 @@ function create_ddl_file() {
echo $tmp
}
+
+function create_pgfile() {
+ mv -f $HOME/.pgpass $HOME/.pgpass_bak > /dev/null 2>&1
+ echo "$HOST:$PORT:*:$USER:$PWD" > $HOME/.pgpass
+ chmod 600 $HOME/.pgpass
+}
+
+
+function clean_pgfile() {
+ rm -f $HOME/.pgpass > /dev/null 2>&1
+ mv -f $HOME/.pgpass_bak $HOME/.pgpass > /dev/null 2>&1
+}
+
+
function cleanup() {
rm -f "/tmp/*.$$"
}
-while getopts ":a:d:u:p:f:t" options; do
- case $options in
- a ) ACTION=$OPTARG;;
- d ) DATABASE=$OPTARG;;
- u ) USER=$OPTARG;;
- p ) PWD=$OPTARG;;
- t ) TEST_ALSO=1;;
- f ) OUTPUT_FILE=$OPTARG;;
- h ) usage;;
- * ) usage;;
+while true; do
+ case "$1" in
+ -a|--action) ACTION=$2; shift 2;;
+ --driver) DRIVER=$2; shift 2;;
+ -d|--database) DATABASE=$2; shift 2;;
+ -h|--host) HOST=$2; shift 2;;
+ --port) HOST=$2; shift 2;;
+ -u|--user) USER=$2; shift 2;;
+ -p|--password) PWD=$2; shift 2;;
+ -t|--test) TEST_ALSO=1; shift 2;;
+ -f|--file) OUTPUT_FILE=$2; shift 2;;
+ --help) usage; shift;;
+ --) shift; break;;
esac
done
-
if [ -z $ACTION ]; then
echo "Need to specify an action "
usage
fi
+if [ $DRIVER != "mysql" ] && [ $DRIVER != "postgres" ]; then
+ echo "Only support driver or "
+ usage
+fi
+
+
+if [ $DRIVER == "mysql" ] && [ -z $PORT ]; then
+ PORT=$PORT_MYSQL
+fi
+if [ $DRIVER == "postgres" ] && [ -z $PORT ]; then
+ PORT=$PORT_POSTGRES
+fi
+
+
if [ $ACTION == "dump" ]; then
DDL_FILE=`create_ddl_file`
if [ -z $OUTPUT_FILE ]; then
@@ -150,17 +206,31 @@ if [ $ACTION == "dump" ]; then
fi
fi
+
if [ $ACTION == "create" ]; then
DDL_FILE=`create_ddl_file`
- echo "Applying new schema $tmp to database $DATABASE"
- mysql -u $USER --password=$PWD < $DDL_FILE
+ echo "Applying new schema to database $DATABASE"
+ if [ $DRIVER == "mysql" ]; then
+ mysql -h $HOST -P $PORT -u $USER --password=$PWD < $DDL_FILE
+ else
+ create_pgfile
+ psql -h $HOST -p $PORT -U $USER -d $DATABASE < $DDL_FILE
+ clean_pgfile
+ fi
fi
+
if [ $ACTION == "clean" ]; then
DDL_FILE=`create_ddl_file`
CLEAN_FILE=`create_clean_file $DDL_FILE`
echo "Cleaning db tables on database $DATABASE"
- mysql -u $USER --password=$PWD < $DDL_FILE
+ if [ $DRIVER == "mysql" ]; then
+ mysql -h $HOST -P $PORT -u $USER --password=$PWD < $CLEAN_FILE
+ else
+ create_pgfile
+ psql -h $HOST -p $PORT -U $USER -d $DATABASE < $CLEAN_FILE
+ clean_pgfile
+ fi
fi
cleanup
diff --git a/bin/start-server b/bin/start-server
index 4327f7af04..064ea1308b 100755
--- a/bin/start-server
+++ b/bin/start-server
@@ -30,7 +30,7 @@ DEBUG_OPTS_ECLIPSE=" -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,ad
DEBUG_OPTS_ECLIPSE_WAIT=" -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=12345 "
# Default JVM settings if unset
-MAVEN_OPTS=${MAVEN_OPTS-"-Duser.timezone=GMT -Xms512m -Xmx1024m -XX:MaxPermSize=512m -XX:MaxDirectMemorySize=512m -XX:+UseConcMarkSweepGC"}
+MAVEN_OPTS=${MAVEN_OPTS-"-Duser.timezone=GMT -Dorg.killbill.server.http.gzip=true -Xms512m -Xmx1024m -XX:MaxPermSize=512m -XX:MaxDirectMemorySize=512m -XX:+UseConcMarkSweepGC"}
LOG="$SERVER/src/main/resources/logback.xml"
LOG_DIR="$SERVER/logs"
diff --git a/catalog/pom.xml b/catalog/pom.xml
index abdc1b92c5..ef0cf8a6e3 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -19,7 +19,7 @@
killbill
org.kill-bill.billing
- 0.15.3-SNAPSHOT
+ 0.15.7-SNAPSHOT
../pom.xml
killbill-catalog
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/io/VersionedCatalogLoader.java b/catalog/src/main/java/org/killbill/billing/catalog/io/VersionedCatalogLoader.java
index 5d0e820c68..3404ec7938 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/io/VersionedCatalogLoader.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/io/VersionedCatalogLoader.java
@@ -63,20 +63,7 @@ public VersionedCatalog loadDefaultCatalog(final String uriString) throws Catalo
List xmlURIs;
if (uriString.endsWith(XML_EXTENSION)) { // Assume its an xml file
xmlURIs = new ArrayList();
- URI uri = new URI(uriString);
-
- // Try to expand the full path, if possible
- final String schemeSpecificPart = uri.getSchemeSpecificPart();
- if (schemeSpecificPart != null) {
- final String[] split = schemeSpecificPart.split("/");
- final String fileName = split[split.length - 1];
- try {
- uri = new URI(Resources.getResource(fileName).toExternalForm());
- } catch (IllegalArgumentException ignored) {
- }
- }
-
- xmlURIs.add(uri);
+ xmlURIs.add(new URI(uriString));
} else { // Assume its a directory
final String directoryContents = UriAccessor.accessUriAsString(uriString);
xmlURIs = findXmlReferences(directoryContents, new URL(uriString));
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/io/TestVersionedCatalogLoader.java b/catalog/src/test/java/org/killbill/billing/catalog/io/TestVersionedCatalogLoader.java
index d6f212ea38..ef0d6456ac 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/io/TestVersionedCatalogLoader.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/io/TestVersionedCatalogLoader.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * 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
@@ -18,6 +18,7 @@
package org.killbill.billing.catalog.io;
+import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
@@ -39,6 +40,7 @@
import org.testng.annotations.Test;
import org.xml.sax.SAXException;
+import com.google.common.io.Files;
import com.google.common.io.Resources;
public class TestVersionedCatalogLoader extends CatalogTestSuiteNoDB {
@@ -126,4 +128,42 @@ public void testLoad() throws IOException, SAXException, InvalidConfigException,
dt = new DateTime("2011-03-03T00:00:00+00:00");
Assert.assertEquals(it.next().getEffectiveDate(), dt.toDate());
}
+
+ @Test(groups = "fast")
+ public void testLoadCatalogFromClasspathResourceFolder() throws CatalogApiException {
+ final VersionedCatalog c = loader.loadDefaultCatalog("SpyCarBasic.xml");
+ Assert.assertEquals(c.size(), 1);
+ final DateTime dt = new DateTime("2013-02-08T00:00:00+00:00");
+ Assert.assertEquals(c.getEffectiveDate(), dt.toDate());
+ Assert.assertEquals(c.getCatalogName(), "SpyCarBasic");
+ }
+
+ @Test(groups = "fast", expectedExceptions = CatalogApiException.class)
+ public void testLoadCatalogFromClasspathResourceBadFolder() throws CatalogApiException {
+ loader.loadDefaultCatalog("SpyCarCustom.xml");
+ }
+
+ @Test(groups = "fast")
+ public void testLoadCatalogFromInsideResourceFolder() throws CatalogApiException, URISyntaxException, IOException {
+ final VersionedCatalog c = loader.loadDefaultCatalog("com/acme/SpyCarCustom.xml");
+ Assert.assertEquals(c.size(), 1);
+ final DateTime dt = new DateTime("2015-10-04T00:00:00+00:00");
+ Assert.assertEquals(c.getEffectiveDate(), dt.toDate());
+ Assert.assertEquals(c.getCatalogName(), "SpyCarCustom");
+ }
+
+ @Test(groups = "fast", expectedExceptions = CatalogApiException.class)
+ public void testLoadCatalogFromInsideResourceWithBadFolderName() throws CatalogApiException {
+ loader.loadDefaultCatalog("com/acme2/SpyCarCustom.xml");
+ }
+
+ @Test(groups = "fast")
+ public void testLoadCatalogFromExternalFile() throws CatalogApiException, IOException, URISyntaxException {
+ final File originFile = new File(Resources.getResource("SpyCarBasic.xml").toURI());
+ final File destinationFile = new File(Files.createTempDir().toString() + "/SpyCarBasicRelocated.xml");
+ destinationFile.deleteOnExit();
+ Files.copy(originFile, destinationFile);
+ final VersionedCatalog c = loader.loadDefaultCatalog(destinationFile.toURI().toString());
+ Assert.assertEquals(c.getCatalogName(), "SpyCarBasic");
+ }
}
diff --git a/catalog/src/test/resources/SpyCarAdvanced.xml b/catalog/src/test/resources/SpyCarAdvanced.xml
index be7c2c67cf..f02a9cdce4 100644
--- a/catalog/src/test/resources/SpyCarAdvanced.xml
+++ b/catalog/src/test/resources/SpyCarAdvanced.xml
@@ -292,6 +292,51 @@
+
+ Sports
+
+
+
+ DAYS
+ 30
+
+
+
+
+
+
+
+
+
+ UNLIMITED
+
+
+ ANNUAL
+
+
+ GBP
+ 3750.00
+
+
+ EUR
+ 4250.00
+
+
+ USD
+ 5000.00
+
+
+ JPY
+ 500.00
+
+
+ BTC
+ 5.0
+
+
+
+
+
Super
@@ -611,6 +656,10 @@
GBP
5.95
+
+ EUR
+ 6.95
+
USD
7.95
@@ -745,6 +794,7 @@
standard-annual
standard-monthly
+ sports-annual
sports-monthly
super-monthly
remotecontrol-monthly
diff --git a/catalog/src/test/resources/com/acme/SpyCarCustom.xml b/catalog/src/test/resources/com/acme/SpyCarCustom.xml
new file mode 100644
index 0000000000..2d29b02b74
--- /dev/null
+++ b/catalog/src/test/resources/com/acme/SpyCarCustom.xml
@@ -0,0 +1,189 @@
+
+
+
+
+
+ 2015-10-04T00:00:00+00:00
+ SpyCarCustom
+
+ IN_ADVANCE
+
+
+ USD
+ GBP
+
+
+
+
+ BASE
+
+
+ BASE
+
+
+ BASE
+
+
+
+
+
+
+ IMMEDIATE
+
+
+
+
+ START_OF_BUNDLE
+
+
+
+
+ IMMEDIATE
+
+
+
+
+ START_OF_BUNDLE
+
+
+
+
+ ACCOUNT
+
+
+
+
+ DEFAULT
+
+
+
+
+
+
+ Standard
+
+
+
+ DAYS
+ 30
+
+
+
+
+
+
+
+
+
+
+ UNLIMITED
+
+
+ MONTHLY
+
+
+ GBP
+ 75.00
+
+
+ USD
+ 111.00
+
+
+
+
+
+
+ Sports
+
+
+
+ DAYS
+ 30
+
+
+
+
+
+
+
+
+
+ UNLIMITED
+
+
+ MONTHLY
+
+
+ GBP
+ 375.00
+
+
+ USD
+ 511.00
+
+
+
+
+
+
+ Super
+
+
+
+ DAYS
+ 30
+
+
+
+
+
+
+
+
+
+
+ UNLIMITED
+
+
+ MONTHLY
+
+
+ GBP
+ 750.00
+
+
+ USD
+ 1111.00
+
+
+
+
+
+
+
+
+
+ standard-monthly
+ sports-monthly
+ super-monthly
+
+
+
+
diff --git a/currency/pom.xml b/currency/pom.xml
index 62ea8e3846..c53932f8db 100644
--- a/currency/pom.xml
+++ b/currency/pom.xml
@@ -19,7 +19,7 @@
killbill
org.kill-bill.billing
- 0.15.3-SNAPSHOT
+ 0.15.7-SNAPSHOT
../pom.xml
killbill-currency
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index ea853a24b5..71bb94697b 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -19,7 +19,7 @@
killbill
org.kill-bill.billing
- 0.15.3-SNAPSHOT
+ 0.15.7-SNAPSHOT
../pom.xml
killbill-entitlement
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
index 7f2ecc60c1..0157feffea 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
@@ -26,9 +26,9 @@
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
-import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
@@ -303,7 +303,7 @@ public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext
}
final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.getBundleFromId(bundleId, contextWithValidAccountRecordId);
- final Account account = accountApi.getAccountById(bundle.getAccountId(), contextWithValidAccountRecordId);
+ final ImmutableAccountData account = accountApi.getImmutableAccountDataById(bundle.getAccountId(), contextWithValidAccountRecordId);
final SubscriptionBase baseSubscription = subscriptionBaseInternalApi.getBaseSubscription(bundleId, contextWithValidAccountRecordId);
final DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getEffectiveDate(), baseSubscription.getStartDate(), contextWithValidAccountRecordId);
@@ -358,7 +358,7 @@ public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext
try {
final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(bundleId, ObjectType.BUNDLE, context);
final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.getBundleFromId(bundleId, contextWithValidAccountRecordId);
- final Account account = accountApi.getAccountById(bundle.getAccountId(), contextWithValidAccountRecordId);
+ final ImmutableAccountData account = accountApi.getImmutableAccountDataById(bundle.getAccountId(), contextWithValidAccountRecordId);
final SubscriptionBase baseSubscription = subscriptionBaseInternalApi.getBaseSubscription(bundleId, contextWithValidAccountRecordId);
final DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getEffectiveDate(), baseSubscription.getStartDate(), contextWithValidAccountRecordId);
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementDateHelper.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementDateHelper.java
index 3b0b63b3cc..7d6272c4b3 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementDateHelper.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementDateHelper.java
@@ -18,12 +18,10 @@
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
-import org.joda.time.Interval;
import org.joda.time.LocalDate;
-
-import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.clock.Clock;
import org.killbill.clock.ClockUtil;
@@ -40,7 +38,7 @@ public EntitlementDateHelper(final AccountInternalApi accountApi, final Clock cl
public DateTime fromLocalDateAndReferenceTime(final LocalDate requestedDate, final DateTime referenceDateTime, final InternalTenantContext callContext) throws EntitlementApiException {
try {
- final Account account = accountApi.getAccountByRecordId(callContext.getAccountRecordId(), callContext);
+ final ImmutableAccountData account = accountApi.getImmutableAccountDataByRecordId(callContext.getAccountRecordId(), callContext);
return ClockUtil.computeDateTimeWithUTCReferenceTime(requestedDate, referenceDateTime.toDateTime(DateTimeZone.UTC).toLocalTime(), account.getTimeZone(), clock);
} catch (AccountApiException e) {
throw new EntitlementApiException(e);
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEntitlements.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEntitlements.java
index f86b5872ae..deca457f34 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEntitlements.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEntitlements.java
@@ -20,7 +20,7 @@
import java.util.Map;
import java.util.UUID;
-import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.entitlement.AccountEntitlements;
import org.killbill.billing.entitlement.AccountEventsStreams;
import org.killbill.billing.entitlement.api.Entitlement;
@@ -37,7 +37,7 @@ public DefaultAccountEntitlements(final AccountEventsStreams accountEventsStream
}
@Override
- public Account getAccount() {
+ public ImmutableAccountData getAccount() {
return accountEventsStreams.getAccount();
}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEventsStreams.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEventsStreams.java
index df46e81f8b..d1b09a8455 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEventsStreams.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEventsStreams.java
@@ -21,7 +21,7 @@
import java.util.Map;
import java.util.UUID;
-import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.entitlement.AccountEventsStreams;
import org.killbill.billing.entitlement.EventsStream;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
@@ -31,11 +31,11 @@
public class DefaultAccountEventsStreams implements AccountEventsStreams {
- private final Account account;
+ private final ImmutableAccountData account;
private final Map> eventsStreams;
private final Map bundles = new HashMap();
- public DefaultAccountEventsStreams(final Account account,
+ public DefaultAccountEventsStreams(final ImmutableAccountData account,
final Iterable bundles,
final Map> eventsStreams) {
this.account = account;
@@ -45,12 +45,12 @@ public DefaultAccountEventsStreams(final Account account,
}
}
- public DefaultAccountEventsStreams(final Account account) {
+ public DefaultAccountEventsStreams(final ImmutableAccountData account) {
this(account, ImmutableList.of(), ImmutableMap.>of());
}
@Override
- public Account getAccount() {
+ public ImmutableAccountData getAccount() {
return account;
}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/dao/OptimizedProxyBlockingStateDao.java b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/OptimizedProxyBlockingStateDao.java
index 1457e63419..cef565e031 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/dao/OptimizedProxyBlockingStateDao.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/OptimizedProxyBlockingStateDao.java
@@ -21,12 +21,9 @@
import javax.annotation.Nullable;
-import org.skife.jdbi.v2.IDBI;
-
-import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.ProductCategory;
-import org.killbill.clock.Clock;
import org.killbill.billing.entitlement.EventsStream;
import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
@@ -37,6 +34,8 @@
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.clock.Clock;
+import org.skife.jdbi.v2.IDBI;
import com.google.common.collect.ImmutableList;
@@ -57,22 +56,20 @@ public OptimizedProxyBlockingStateDao(final EventsStreamBuilder eventsStreamBuil
*
* This is a special method for EventsStreamBuilder to save some DAO calls.
*
- * @param subscriptionBlockingStatesOnDisk
- * blocking states on disk for that subscription
- * @param allBlockingStatesOnDiskForAccount
- * all blocking states on disk for that account
- * @param account account associated with the subscription
- * @param bundle bundle associated with the subscription
- * @param baseSubscription base subscription (ProductCategory.BASE) associated with that bundle
- * @param subscription subscription for which to build blocking states
- * @param allSubscriptionsForBundle all subscriptions associated with that bundle
- * @param context call context
+ * @param subscriptionBlockingStatesOnDisk blocking states on disk for that subscription
+ * @param allBlockingStatesOnDiskForAccount all blocking states on disk for that account
+ * @param account account associated with the subscription
+ * @param bundle bundle associated with the subscription
+ * @param baseSubscription base subscription (ProductCategory.BASE) associated with that bundle
+ * @param subscription subscription for which to build blocking states
+ * @param allSubscriptionsForBundle all subscriptions associated with that bundle
+ * @param context call context
* @return blocking states for that subscription
* @throws EntitlementApiException
*/
public List getBlockingHistory(final List subscriptionBlockingStatesOnDisk,
final List allBlockingStatesOnDiskForAccount,
- final Account account,
+ final ImmutableAccountData account,
final SubscriptionBaseBundle bundle,
@Nullable final SubscriptionBase baseSubscription,
final SubscriptionBase subscription,
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
index 192d301478..23795b6b5a 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
@@ -27,7 +27,7 @@
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
-import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.Product;
@@ -55,7 +55,7 @@
public class DefaultEventsStream implements EventsStream {
- private final Account account;
+ private final ImmutableAccountData account;
private final SubscriptionBaseBundle bundle;
// All blocking states for the account, associated bundle or subscription
private final List blockingStates;
@@ -80,7 +80,7 @@ public class DefaultEventsStream implements EventsStream {
private BlockingState entitlementCancelEvent;
private EntitlementState entitlementState;
- public DefaultEventsStream(final Account account, final SubscriptionBaseBundle bundle,
+ public DefaultEventsStream(final ImmutableAccountData account, final SubscriptionBaseBundle bundle,
final List blockingStates, final BlockingChecker blockingChecker,
@Nullable final SubscriptionBase baseSubscription, final SubscriptionBase subscription,
final List allSubscriptionsForBundle, final InternalTenantContext contextWithValidAccountRecordId, final DateTime utcNow) {
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java
index 8bb42d5ac3..afd51c895c 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java
@@ -29,9 +29,9 @@
import javax.inject.Singleton;
import org.killbill.billing.ObjectType;
-import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.AccountEventsStreams;
@@ -123,9 +123,9 @@ public AccountEventsStreams buildForAccount(final InternalTenantContext internal
// Special signature for ProxyBlockingStateDao to save a DAO call
public AccountEventsStreams buildForAccount(final Map> subscriptions, final InternalTenantContext internalTenantContext) throws EntitlementApiException {
// Retrieve the account
- final Account account;
+ final ImmutableAccountData account;
try {
- account = accountInternalApi.getAccountByRecordId(internalTenantContext.getAccountRecordId(), internalTenantContext);
+ account = accountInternalApi.getImmutableAccountDataByRecordId(internalTenantContext.getAccountRecordId(), internalTenantContext);
} catch (AccountApiException e) {
throw new EntitlementApiException(e);
}
@@ -229,9 +229,9 @@ public EventsStream buildForEntitlement(final UUID entitlementId, final Internal
throw new EntitlementApiException(e);
}
- final Account account;
+ final ImmutableAccountData account;
try {
- account = accountInternalApi.getAccountById(bundle.getAccountId(), internalTenantContext);
+ account = accountInternalApi.getImmutableAccountDataById(bundle.getAccountId(), internalTenantContext);
} catch (AccountApiException e) {
throw new EntitlementApiException(e);
}
@@ -244,7 +244,7 @@ public EventsStream buildForEntitlement(final UUID entitlementId, final Internal
// Special signature for OptimizedProxyBlockingStateDao to save some DAO calls
public EventsStream buildForEntitlement(final List blockingStatesForAccount,
- final Account account,
+ final ImmutableAccountData account,
final SubscriptionBaseBundle bundle,
final SubscriptionBase baseSubscription,
final List allSubscriptionsForBundle,
@@ -253,7 +253,7 @@ public EventsStream buildForEntitlement(final List blockingStates
}
private EventsStream buildForEntitlement(final List blockingStatesForAccount,
- final Account account,
+ final ImmutableAccountData account,
final SubscriptionBaseBundle bundle,
@Nullable final SubscriptionBase baseSubscription,
final SubscriptionBase subscription,
@@ -312,7 +312,7 @@ private EventsStream buildForEntitlement(final List blockingState
return buildForEntitlement(account, bundle, baseSubscription, subscription, allSubscriptionsForBundle, blockingStates, internalTenantContext);
}
- private EventsStream buildForEntitlement(final Account account,
+ private EventsStream buildForEntitlement(final ImmutableAccountData account,
final SubscriptionBaseBundle bundle,
@Nullable final SubscriptionBase baseSubscription,
final SubscriptionBase subscription,
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestEntitlementDateHelper.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestEntitlementDateHelper.java
index 9a586a1914..ae889c93f8 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestEntitlementDateHelper.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestEntitlementDateHelper.java
@@ -42,6 +42,7 @@ public void beforeMethod() throws Exception {
account = Mockito.mock(Account.class);
Mockito.when(accountInternalApi.getAccountByRecordId(Mockito.anyLong(), Mockito.any())).thenReturn(account);
+ Mockito.when(accountInternalApi.getImmutableAccountDataByRecordId(Mockito.anyLong(), Mockito.any())).thenReturn(account);
dateHelper = new EntitlementDateHelper(accountInternalApi, clock);
clock.resetDeltaFromReality();;
}
diff --git a/invoice/pom.xml b/invoice/pom.xml
index 6b48d16abe..a8fe578da6 100644
--- a/invoice/pom.xml
+++ b/invoice/pom.xml
@@ -19,7 +19,7 @@
killbill
org.kill-bill.billing
- 0.15.3-SNAPSHOT
+ 0.15.7-SNAPSHOT
../pom.xml
killbill-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 d229093938..8ddc243ca2 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -22,6 +22,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -36,15 +37,14 @@
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
-import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
-import org.killbill.billing.catalog.api.Usage;
import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.events.BusInternalEvent;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
@@ -54,6 +54,7 @@
import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
import org.killbill.billing.invoice.api.DefaultInvoiceService;
import org.killbill.billing.invoice.api.DryRunArguments;
+import org.killbill.billing.invoice.api.DryRunType;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
@@ -66,8 +67,10 @@
import org.killbill.billing.invoice.dao.InvoiceDao;
import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
import org.killbill.billing.invoice.dao.InvoiceModelDao;
-import org.killbill.billing.invoice.generator.BillingIntervalDetail;
import org.killbill.billing.invoice.generator.InvoiceGenerator;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates.UsageDef;
import org.killbill.billing.invoice.model.DefaultInvoice;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
import org.killbill.billing.invoice.model.InvoiceItemFactory;
@@ -99,7 +102,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
@@ -113,11 +115,10 @@
public class InvoiceDispatcher {
private static final Logger log = LoggerFactory.getLogger(InvoiceDispatcher.class);
- private static final int NB_LOCK_TRY = 5;
private static final Ordering UPCOMING_NOTIFICATION_DATE_ORDERING = Ordering.natural();
-
- private static final NullDryRunArguments NULL_DRY_RUN_ARGUMENTS = new NullDryRunArguments();
+ private final static Joiner JOINER_COMMA = Joiner.on(",");
+ private static final TargetDateDryRunArguments TARGET_DATE_DRY_RUN_ARGUMENTS = new TargetDateDryRunArguments();
private final InvoiceGenerator generator;
private final BillingInternalApi billingApi;
@@ -193,7 +194,7 @@ private Invoice processSubscriptionInternal(final UUID subscriptionId, final Dat
return null;
}
final UUID accountId = subscriptionApi.getAccountIdFromSubscriptionId(subscriptionId, context);
- final DryRunArguments dryRunArguments = dryRunForNotification ? NULL_DRY_RUN_ARGUMENTS : null;
+ final DryRunArguments dryRunArguments = dryRunForNotification ? TARGET_DATE_DRY_RUN_ARGUMENTS : null;
return processAccount(accountId, targetDate, dryRunArguments, context);
} catch (final SubscriptionBaseApiException e) {
@@ -203,11 +204,11 @@ private Invoice processSubscriptionInternal(final UUID subscriptionId, final Dat
}
}
- public Invoice processAccount(final UUID accountId, final DateTime targetDate,
+ public Invoice processAccount(final UUID accountId, @Nullable final DateTime targetDate,
@Nullable final DryRunArguments dryRunArguments, final InternalCallContext context) throws InvoiceApiException {
GlobalLock lock = null;
try {
- lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), NB_LOCK_TRY);
+ lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), invoiceConfig.getMaxGlobalLockRetries());
return processAccountWithLock(accountId, targetDate, dryRunArguments, context);
} catch (final LockFailedException e) {
@@ -226,16 +227,23 @@ private Invoice processAccountWithLock(final UUID accountId, @Nullable final Dat
@Nullable final DryRunArguments dryRunArguments, final InternalCallContext context) throws InvoiceApiException {
final boolean isDryRun = dryRunArguments != null;
- // inputTargetDateTime is only allowed in dryRun mode to have the system compute it
- Preconditions.checkArgument(inputTargetDateTime != null || isDryRun, "inputTargetDateTime is required in non dryRun mode");
+ // A null inputTargetDateTime is only allowed in dryRun mode to have the system compute it
+ Preconditions.checkArgument(inputTargetDateTime != null ||
+ (dryRunArguments != null && DryRunType.UPCOMING_INVOICE.equals(dryRunArguments.getDryRunType())), "inputTargetDateTime is required in non dryRun mode");
try {
// Make sure to first set the BCD if needed then get the account object (to have the BCD set)
final BillingEventSet billingEvents = billingApi.getBillingEventsForAccountAndUpdateAccountBCD(accountId, dryRunArguments, context);
-
- final List candidateDateTimes = (inputTargetDateTime != null) ? ImmutableList.of(inputTargetDateTime) : getUpcomingInvoiceCandidateDates(context);
+ if (billingEvents.isEmpty()) {
+ return null;
+ }
+ final Iterable filteredSubscriptionIdsForDryRun = getFilteredSubscriptionIdsForDryRun(dryRunArguments, billingEvents);
+ final List candidateDateTimes = (inputTargetDateTime != null) ?
+ ImmutableList.of(inputTargetDateTime) :
+ getUpcomingInvoiceCandidateDates(filteredSubscriptionIdsForDryRun, context);
for (final DateTime curTargetDateTime : candidateDateTimes) {
final Invoice invoice = processAccountWithLockAndInputTargetDate(accountId, curTargetDateTime, billingEvents, isDryRun, context);
if (invoice != null) {
+ filterInvoiceItemsForDryRun(filteredSubscriptionIdsForDryRun, invoice);
return invoice;
}
}
@@ -246,13 +254,50 @@ private Invoice processAccountWithLock(final UUID accountId, @Nullable final Dat
}
}
+ private void filterInvoiceItemsForDryRun(final Iterable filteredSubscriptionIdsForDryRun, final Invoice invoice) {
+ if (!filteredSubscriptionIdsForDryRun.iterator().hasNext()) {
+ return;
+ }
+
+ final Iterator it = invoice.getInvoiceItems().iterator();
+ while (it.hasNext()) {
+ final InvoiceItem cur = it.next();
+ if (!Iterables.contains(filteredSubscriptionIdsForDryRun, cur.getSubscriptionId())) {
+ it.remove();
+ }
+ }
+ }
+
+ private Iterable getFilteredSubscriptionIdsForDryRun(@Nullable final DryRunArguments dryRunArguments, final BillingEventSet billingEvents) {
+ if (dryRunArguments == null ||
+ !dryRunArguments.getDryRunType().equals(DryRunType.UPCOMING_INVOICE) ||
+ (dryRunArguments.getSubscriptionId() == null && dryRunArguments.getBundleId() == null)) {
+ return ImmutableList.of();
+ }
+
+ if (dryRunArguments.getSubscriptionId() != null) {
+ return ImmutableList.of(dryRunArguments.getSubscriptionId());
+ }
+
+ return Iterables.transform(Iterables.filter(billingEvents, new Predicate() {
+ @Override
+ public boolean apply(final BillingEvent input) {
+ return input.getSubscription().getBundleId().equals(dryRunArguments.getBundleId());
+ }
+ }), new Function() {
+ @Override
+ public UUID apply(final BillingEvent input) {
+ return input.getSubscription().getId();
+ }
+ });
+ }
+
private Invoice processAccountWithLockAndInputTargetDate(final UUID accountId, final DateTime targetDateTime,
final BillingEventSet billingEvents, final boolean isDryRun, final InternalCallContext context) throws InvoiceApiException {
try {
- final Account account = accountApi.getAccountById(accountId, context);
- final DateAndTimeZoneContext dateAndTimeZoneContext = billingEvents.iterator().hasNext() ?
- new DateAndTimeZoneContext(billingEvents.iterator().next().getEffectiveDate(), account.getTimeZone(), clock) :
- null;
+ final ImmutableAccountData account = accountApi.getImmutableAccountDataById(accountId, context);
+
+ final DateAndTimeZoneContext dateAndTimeZoneContext = new DateAndTimeZoneContext(billingEvents.iterator().next().getEffectiveDate(), account.getTimeZone(), clock);
final List invoices = billingEvents.isAccountAutoInvoiceOff() ?
ImmutableList.of() :
@@ -265,20 +310,30 @@ public Invoice apply(final InvoiceModelDao input) {
}));
final Currency targetCurrency = account.getCurrency();
+ final LocalDate targetDate = dateAndTimeZoneContext.computeTargetDate(targetDateTime);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, billingEvents, invoices, targetDate, targetCurrency, context);
+ final Invoice invoice = invoiceWithMetadata.getInvoice();
+
+ // Compute future notifications
+ final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceWithMetadata, dateAndTimeZoneContext, context);
- final LocalDate targetDate = (dateAndTimeZoneContext != null && targetDateTime != null) ? dateAndTimeZoneContext.computeTargetDate(targetDateTime) : null;
- final Invoice invoice = targetDate != null ? generator.generateInvoice(account, billingEvents, invoices, targetDate, targetCurrency, context) : null;
//
+
// If invoice comes back null, there is nothing new to generate, we can bail early
//
if (invoice == null) {
- log.info("Generated null invoice for accountId {} and targetDate {} (targetDateTime {})", new Object[]{accountId, targetDate, targetDateTime});
- if (!isDryRun) {
+ if (isDryRun) {
+ log.info("Generated null dryRun invoice for accountId {} and targetDate {} (targetDateTime {})", new Object[]{accountId, targetDate, targetDateTime});
+ } else {
+ log.info("Generated null invoice for accountId {} and targetDate {} (targetDateTime {})", new Object[]{accountId, targetDate, targetDateTime});
+
final BusInternalEvent event = new DefaultNullInvoiceEvent(accountId, clock.getUTCToday(),
context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+
+ commitInvoiceAndSetFutureNotifications(account, null, ImmutableList.of(), futureAccountNotifications, false, context);
postEvent(event, accountId, context);
}
- return invoice;
+ return null;
}
// Generate missing credit (> 0 for generation and < 0 for use) prior we call the plugin
@@ -286,14 +341,36 @@ public Invoice apply(final InvoiceModelDao input) {
if (cbaItem != null) {
invoice.addInvoiceItem(cbaItem);
}
-
//
// Ask external invoice plugins if additional items (tax, etc) shall be added to the invoice
//
final CallContext callContext = buildCallContext(context);
- invoice.addInvoiceItems(invoicePluginDispatcher.getAdditionalInvoiceItems(invoice, callContext));
+ invoice.addInvoiceItems(invoicePluginDispatcher.getAdditionalInvoiceItems(invoice, isDryRun, callContext));
+
if (!isDryRun) {
- commitInvoiceStateAndNotifyAccountIfConfigured(account, invoice, billingEvents, dateAndTimeZoneContext, targetDate, context);
+
+ // Compute whether this is a new invoice object (or just some adjustments on an existing invoice), and extract invoiceIds for later use
+ final Set uniqueInvoiceIds = getUniqueInvoiceIds(invoice);
+ final boolean isRealInvoiceWithItems = uniqueInvoiceIds.remove(invoice.getId());
+ final Set adjustedUniqueOtherInvoiceId = uniqueInvoiceIds;
+
+ logInvoiceWithItems(account, invoice, targetDate, adjustedUniqueOtherInvoiceId, isRealInvoiceWithItems);
+
+ // Transformation to Invoice -> InvoiceModelDao
+ final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoice);
+ final Iterable invoiceItemModelDaos = transformToInvoiceModelDao(invoice.getInvoiceItems());
+
+ // Commit invoice on disk
+ final boolean isThereAnyItemsLeft = commitInvoiceAndSetFutureNotifications(account, invoiceModelDao, invoiceItemModelDaos, futureAccountNotifications, isRealInvoiceWithItems, context);
+
+ final boolean isRealInvoiceWithNonEmptyItems = isThereAnyItemsLeft ? isRealInvoiceWithItems : false;
+
+ setChargedThroughDates(dateAndTimeZoneContext, invoice.getInvoiceItems(FixedPriceInvoiceItem.class), invoice.getInvoiceItems(RecurringInvoiceItem.class), context);
+
+ // TODO we should send bus events when we commit the ionvoice on disk in commitInvoice
+ postEvents(account, invoice, adjustedUniqueOtherInvoiceId, isRealInvoiceWithNonEmptyItems, context);
+
+ notifyAccountIfEnabled(account, invoice, isRealInvoiceWithNonEmptyItems, context);
}
return invoice;
} catch (final AccountApiException e) {
@@ -305,38 +382,90 @@ public Invoice apply(final InvoiceModelDao input) {
}
}
- private void commitInvoiceStateAndNotifyAccountIfConfigured(final Account account, final Invoice invoice, final BillingEventSet billingEvents, final DateAndTimeZoneContext dateAndTimeZoneContext, final LocalDate targetDate, final InternalCallContext context) throws SubscriptionBaseApiException, InvoiceApiException {
- boolean isRealInvoiceWithNonEmptyItems = false;
- // Extract the set of invoiceId for which we see items that don't belong to current generated invoice
- final Set adjustedUniqueOtherInvoiceId = new TreeSet();
- adjustedUniqueOtherInvoiceId.addAll(Collections2.transform(invoice.getInvoiceItems(), new Function() {
+ private FutureAccountNotifications createNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final DateAndTimeZoneContext dateAndTimeZoneContext, final InternalCallContext context) {
+
+ final Map> result = new HashMap>();
+
+ for (final UUID subscriptionId : invoiceWithMetadata.getPerSubscriptionFutureNotificationDates().keySet()) {
+
+ final List perSubscriptionNotifications = new ArrayList();
+
+ final SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = invoiceWithMetadata.getPerSubscriptionFutureNotificationDates().get(subscriptionId);
+ // Add next recurring date if any
+ if (subscriptionFutureNotificationDates.getNextRecurringDate() != null) {
+ perSubscriptionNotifications.add(new SubscriptionNotification(dateAndTimeZoneContext.computeUTCDateTimeFromLocalDate(subscriptionFutureNotificationDates.getNextRecurringDate()), true));
+ }
+ // Add next usage dates if any
+ if (subscriptionFutureNotificationDates.getNextUsageDates() != null) {
+ for (UsageDef usageDef : subscriptionFutureNotificationDates.getNextUsageDates().keySet()) {
+ final LocalDate nextNotificationDateForUsage = subscriptionFutureNotificationDates.getNextUsageDates().get(usageDef);
+ final DateTime subscriptionUsageCallbackDate = nextNotificationDateForUsage != null ? dateAndTimeZoneContext.computeUTCDateTimeFromLocalDate(nextNotificationDateForUsage) : null;
+ perSubscriptionNotifications.add(new SubscriptionNotification(subscriptionUsageCallbackDate, true));
+ }
+ }
+ if (!perSubscriptionNotifications.isEmpty()) {
+ result.put(subscriptionId, perSubscriptionNotifications);
+ }
+ }
+
+ // If dryRunNotification is enabled we also need to fetch the upcoming PHASE dates (we add SubscriptionNotification with isForInvoiceNotificationTrigger = false)
+ final boolean isInvoiceNotificationEnabled = invoiceConfig.getDryRunNotificationSchedule().getMillis() > 0;
+ if (isInvoiceNotificationEnabled) {
+ final Map upcomingPhasesForSubscriptions = subscriptionApi.getNextFutureEventForSubscriptions(SubscriptionBaseTransitionType.PHASE, context);
+ for (UUID cur : upcomingPhasesForSubscriptions.keySet()) {
+ final DateTime curDate = upcomingPhasesForSubscriptions.get(cur);
+ List resultValue = result.get(cur);
+ if (resultValue == null) {
+ resultValue = new ArrayList();
+ }
+ resultValue.add(new SubscriptionNotification(curDate, false));
+ result.put(cur, resultValue);
+ }
+ }
+ return new FutureAccountNotifications(dateAndTimeZoneContext, result);
+ }
+
+ private Iterable transformToInvoiceModelDao(final List invoiceItems) {
+ return Iterables.transform(invoiceItems,
+ new Function() {
+ @Override
+ public InvoiceItemModelDao apply(final InvoiceItem input) {
+ return new InvoiceItemModelDao(input);
+ }
+ });
+ }
+
+ private Set getUniqueInvoiceIds(final Invoice invoice) {
+ final Set uniqueInvoiceIds = new TreeSet();
+ uniqueInvoiceIds.addAll(Collections2.transform(invoice.getInvoiceItems(), new Function() {
@Nullable
@Override
public UUID apply(@Nullable final InvoiceItem input) {
return input.getInvoiceId();
}
}));
- boolean isRealInvoiceWithItems = adjustedUniqueOtherInvoiceId.remove(invoice.getId());
+ return uniqueInvoiceIds;
+ }
+
+ private void logInvoiceWithItems(final ImmutableAccountData account, final Invoice invoice, final LocalDate targetDate, final Set adjustedUniqueOtherInvoiceId, final boolean isRealInvoiceWithItems) {
+ final StringBuilder tmp = new StringBuilder();
if (isRealInvoiceWithItems) {
- log.info("Generated invoice {} with {} items for accountId {} and targetDate {}", new Object[]{invoice.getId(), invoice.getNumberOfItems(), account.getId(), targetDate});
+ tmp.append(String.format("Generated invoice %s with %d items for accountId %s and targetDate %s:\n", invoice.getId(), invoice.getNumberOfItems(), account.getId(), targetDate));
} else {
- final Joiner joiner = Joiner.on(",");
- final String adjustedInvoices = joiner.join(adjustedUniqueOtherInvoiceId.toArray(new UUID[adjustedUniqueOtherInvoiceId.size()]));
- log.info("Adjusting existing invoices {} with {} items for accountId {} and targetDate {})", new Object[]{adjustedInvoices, invoice.getNumberOfItems(),
- account.getId(), targetDate});
+ final String adjustedInvoices = JOINER_COMMA.join(adjustedUniqueOtherInvoiceId.toArray(new UUID[adjustedUniqueOtherInvoiceId.size()]));
+ tmp.append(String.format("Adjusting existing invoices %s with %d items for accountId %s and targetDate %s:\n",
+ adjustedInvoices, invoice.getNumberOfItems(), account.getId(), targetDate));
}
+ for (InvoiceItem item : invoice.getInvoiceItems()) {
+ tmp.append(String.format("\t item = %s\n", item));
+ }
+ log.info(tmp.toString());
+ }
- // Transformation to Invoice -> InvoiceModelDao
- final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoice);
- final Iterable invoiceItemModelDaos = Iterables.transform(invoice.getInvoiceItems(),
- new Function() {
- @Override
- public InvoiceItemModelDao apply(final InvoiceItem input) {
- return new InvoiceItemModelDao(input);
- }
- });
- final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceItemModelDaos, billingEvents, dateAndTimeZoneContext, context);
-
+ private boolean commitInvoiceAndSetFutureNotifications(final ImmutableAccountData account, final InvoiceModelDao invoiceModelDao,
+ final Iterable invoiceItemModelDaos,
+ final FutureAccountNotifications futureAccountNotifications,
+ boolean isRealInvoiceWithItems, final InternalCallContext context) throws SubscriptionBaseApiException, InvoiceApiException {
// We filter any zero amount for USAGE items prior we generate the invoice, which may leave us with an invoice with no items;
// we recompute the isRealInvoiceWithItems flag based on what is left (the call to invoice is still necessary to set the future notifications).
final Iterable filteredInvoiceItemModelDaos = Iterables.filter(invoiceItemModelDaos, new Predicate() {
@@ -347,17 +476,15 @@ public boolean apply(@Nullable final InvoiceItemModelDao input) {
});
final boolean isThereAnyItemsLeft = filteredInvoiceItemModelDaos.iterator().hasNext();
- isRealInvoiceWithNonEmptyItems = isThereAnyItemsLeft ? isRealInvoiceWithItems : false;
-
if (isThereAnyItemsLeft) {
invoiceDao.createInvoice(invoiceModelDao, ImmutableList.copyOf(filteredInvoiceItemModelDaos), isRealInvoiceWithItems, futureAccountNotifications, context);
} else {
invoiceDao.setFutureAccountNotificationsForEmptyInvoice(account.getId(), futureAccountNotifications, context);
}
+ return isThereAnyItemsLeft;
+ }
- final List fixedPriceInvoiceItems = invoice.getInvoiceItems(FixedPriceInvoiceItem.class);
- final List recurringInvoiceItems = invoice.getInvoiceItems(RecurringInvoiceItem.class);
- setChargedThroughDates(dateAndTimeZoneContext, fixedPriceInvoiceItems, recurringInvoiceItems, context);
+ private void postEvents(final ImmutableAccountData account, final Invoice invoice, final Set adjustedUniqueOtherInvoiceId, final boolean isRealInvoiceWithNonEmptyItems, final InternalCallContext context) {
final List events = new ArrayList();
if (isRealInvoiceWithNonEmptyItems) {
@@ -370,15 +497,20 @@ public boolean apply(@Nullable final InvoiceItemModelDao input) {
context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
events.add(event);
}
-
for (final InvoiceInternalEvent event : events) {
postEvent(event, account.getId(), context);
}
+ }
- if (account.isNotifiedForInvoices() && isRealInvoiceWithNonEmptyItems) {
+ private void notifyAccountIfEnabled(final ImmutableAccountData account, final Invoice invoice, final boolean isRealInvoiceWithNonEmptyItems, final InternalCallContext context) throws InvoiceApiException, AccountApiException {
+ // Ideally we would retrieve the cached version, all the invoice code has been modified to only use ImmutableAccountData, except for the
+ // isNotifiedForInvoice piece that should probably live outside of invoice code anyways... (see https://github.com/killbill/killbill-email-notifications-plugin)
+ final Account fullAccount = accountApi.getAccountById(account.getId(), context);
+
+ if (fullAccount.isNotifiedForInvoices() && isRealInvoiceWithNonEmptyItems) {
// Need to re-hydrate the invoice object to get the invoice number (record id)
// API_FIX InvoiceNotifier public API?
- invoiceNotifier.notify(account, new DefaultInvoice(invoiceDao.getById(invoice.getId(), context)), buildTenantContext(context));
+ invoiceNotifier.notify(fullAccount, new DefaultInvoice(invoiceDao.getById(invoice.getId(), context)), buildTenantContext(context));
}
}
@@ -405,89 +537,6 @@ private CallContext buildCallContext(final InternalCallContext context) {
return internalCallContextFactory.createCallContext(context);
}
- @VisibleForTesting
- FutureAccountNotifications createNextFutureNotificationDate(final Iterable invoiceItems, final BillingEventSet billingEvents, final DateAndTimeZoneContext dateAndTimeZoneContext, final InternalCallContext context) {
-
- final Map> result = new HashMap>();
-
- final Map perSubscriptionUsage = new HashMap();
-
- // For each subscription that has a positive (amount) recurring item, create the date
- // at which we should be called back for next invoice.
- //
- for (final InvoiceItemModelDao item : invoiceItems) {
-
- List perSubscriptionCallback = result.get(item.getSubscriptionId());
- if (perSubscriptionCallback == null && (item.getType() == InvoiceItemType.RECURRING || item.getType() == InvoiceItemType.USAGE)) {
- perSubscriptionCallback = new ArrayList();
- result.put(item.getSubscriptionId(), perSubscriptionCallback);
- }
-
- switch (item.getType()) {
- case RECURRING:
- if ((item.getEndDate() != null) &&
- (item.getAmount() == null ||
- item.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
- perSubscriptionCallback.add(new SubscriptionNotification(dateAndTimeZoneContext.computeUTCDateTimeFromLocalDate(item.getEndDate()), true));
- }
- break;
-
- case USAGE:
- final String key = item.getSubscriptionId().toString() + ":" + item.getUsageName();
- final LocalDate perSubscriptionUsageRecurringDate = perSubscriptionUsage.get(key);
- if (perSubscriptionUsageRecurringDate == null || perSubscriptionUsageRecurringDate.compareTo(item.getEndDate()) < 0) {
- perSubscriptionUsage.put(key, item.getEndDate());
- }
- break;
-
- default:
- // Ignore
- }
- }
-
- for (final String key : perSubscriptionUsage.keySet()) {
- final String[] parts = key.split(":");
- final UUID subscriptionId = UUID.fromString(parts[0]);
-
- final List perSubscriptionCallback = result.get(subscriptionId);
- final String usageName = parts[1];
- final LocalDate endDate = perSubscriptionUsage.get(key);
-
- final DateTime subscriptionUsageCallbackDate = getNextUsageBillingDate(subscriptionId, usageName, endDate, dateAndTimeZoneContext, billingEvents);
- perSubscriptionCallback.add(new SubscriptionNotification(subscriptionUsageCallbackDate, true));
- }
-
- // If dryRunNotification is enabled we also need to fetch the upcoming PHASE dates (we add SubscriptionNotification with isForInvoiceNotificationTrigger = false)
- final boolean isInvoiceNotificationEnabled = invoiceConfig.getDryRunNotificationSchedule().getMillis() > 0;
- if (isInvoiceNotificationEnabled) {
- final Map upcomingPhasesForSubscriptions = subscriptionApi.getNextFutureEventForSubscriptions(SubscriptionBaseTransitionType.PHASE, context);
- for (UUID cur : upcomingPhasesForSubscriptions.keySet()) {
- final DateTime curDate = upcomingPhasesForSubscriptions.get(cur);
- List resultValue = result.get(cur);
- if (resultValue == null) {
- resultValue = new ArrayList();
- }
- resultValue.add(new SubscriptionNotification(curDate, false));
- result.put(cur, resultValue);
- }
- }
- return new FutureAccountNotifications(dateAndTimeZoneContext, result);
- }
-
- private DateTime getNextUsageBillingDate(final UUID subscriptionId, final String usageName, final LocalDate chargedThroughDate, final DateAndTimeZoneContext dateAndTimeZoneContext, final BillingEventSet billingEvents) {
-
- final Usage usage = billingEvents.getUsages().get(usageName);
- final BillingEvent billingEventSubscription = Iterables.tryFind(billingEvents, new Predicate() {
- @Override
- public boolean apply(@Nullable final BillingEvent input) {
- return input.getSubscription().getId().equals(subscriptionId);
- }
- }).orNull();
-
- final LocalDate nextCallbackUsageDate = (usage.getBillingMode() == BillingMode.IN_ARREAR) ? BillingIntervalDetail.alignProposedBillCycleDate(chargedThroughDate.plusMonths(usage.getBillingPeriod().getNumberOfMonths()), billingEventSubscription.getBillCycleDayLocal()) : chargedThroughDate;
- return dateAndTimeZoneContext.computeUTCDateTimeFromLocalDate(nextCallbackUsageDate);
- }
-
private void setChargedThroughDates(final DateAndTimeZoneContext dateAndTimeZoneContext,
final Collection fixedPriceItems,
final Collection recurringItems,
@@ -554,7 +603,6 @@ public static class SubscriptionNotification {
private final DateTime effectiveDate;
private final boolean isForNotificationTrigger;
-
public SubscriptionNotification(final DateTime effectiveDate, final boolean isForNotificationTrigger) {
this.effectiveDate = effectiveDate;
this.isForNotificationTrigger = isForNotificationTrigger;
@@ -570,30 +618,31 @@ public boolean isForInvoiceNotificationTrigger() {
}
}
- private List getUpcomingInvoiceCandidateDates(final InternalCallContext internalCallContext) {
- final Iterable nextScheduledInvoiceDates = getNextScheduledInvoiceEffectiveDate(internalCallContext);
+ private List getUpcomingInvoiceCandidateDates(final Iterable filteredSubscriptionIds, final InternalCallContext internalCallContext) {
+ final Iterable nextScheduledInvoiceDates = getNextScheduledInvoiceEffectiveDate(filteredSubscriptionIds, internalCallContext);
final Iterable nextScheduledSubscriptionsEventDates = subscriptionApi.getFutureNotificationsForAccount(internalCallContext);
Iterables.concat(nextScheduledInvoiceDates, nextScheduledSubscriptionsEventDates);
return UPCOMING_NOTIFICATION_DATE_ORDERING.sortedCopy(Iterables.concat(nextScheduledInvoiceDates, nextScheduledSubscriptionsEventDates));
}
- private Iterable getNextScheduledInvoiceEffectiveDate(final InternalCallContext internalCallContext) {
+ private Iterable getNextScheduledInvoiceEffectiveDate(final Iterable filteredSubscriptionIds, final InternalCallContext internalCallContext) {
try {
final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
final List> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
- final Iterable> filtered = Iterables.filter(futureNotifications, new Predicate>() {
+ final Iterable> allUpcomingEvents = Iterables.filter(futureNotifications, new Predicate>() {
@Override
public boolean apply(@Nullable final NotificationEventWithMetadata input) {
+ final boolean isEventForSubscription = !filteredSubscriptionIds.iterator().hasNext() || Iterables.contains(filteredSubscriptionIds, input.getEvent().getUuidKey());
final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
input.getEvent().isDryRunForInvoiceNotification() : false;
- return !isEventDryRunForNotifications;
+ return isEventForSubscription && !isEventDryRunForNotifications;
}
});
- return Iterables.transform(filtered, new Function, DateTime>() {
+ return Iterables.transform(allUpcomingEvents, new Function, DateTime>() {
@Nullable
@Override
public DateTime apply(@Nullable final NotificationEventWithMetadata input) {
@@ -605,7 +654,12 @@ public DateTime apply(@Nullable final NotificationEventWithMetadata getPlanPhasePriceoverrides() {
+ public List getPlanPhasePriceOverrides() {
return null;
}
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
index 396942ec30..d05b13a5c0 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
@@ -58,13 +58,13 @@ public InvoicePluginDispatcher(final OSGIServiceRegistration p
// If we have multiple plugins there is a question of plugin ordering and also a 'product' questions to decide whether
// subsequent plugins should have access to items added by previous plugins
//
- public List getAdditionalInvoiceItems(final Invoice originalInvoice, final CallContext callContext) throws InvoiceApiException {
+ public List getAdditionalInvoiceItems(final Invoice originalInvoice, final boolean isDryRun, final CallContext callContext) throws InvoiceApiException {
// We clone the original invoice so plugins don't remove/add items
final Invoice clonedInvoice = (Invoice) ((DefaultInvoice) originalInvoice).clone();
final List additionalInvoiceItems = new LinkedList();
final List invoicePlugins = getInvoicePlugins();
for (final InvoicePluginApi invoicePlugin : invoicePlugins) {
- final List items = invoicePlugin.getAdditionalInvoiceItems(clonedInvoice, ImmutableList.of(), callContext);
+ final List items = invoicePlugin.getAdditionalInvoiceItems(clonedInvoice, isDryRun, ImmutableList.of(), callContext);
if (items != null) {
for (final InvoiceItem item : items) {
validateInvoiceItemFromPlugin(item, invoicePlugin);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java b/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
index 9506d02d08..b6d3059ab4 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
@@ -39,6 +39,7 @@
import org.killbill.billing.util.UUIDs;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.config.InvoiceConfig;
import org.killbill.billing.util.globallocker.LockerType;
import org.killbill.commons.locker.GlobalLock;
import org.killbill.commons.locker.GlobalLocker;
@@ -57,32 +58,32 @@ public class InvoiceApiHelper {
private static final Logger log = LoggerFactory.getLogger(InvoiceApiHelper.class);
- private static final int NB_LOCK_TRY = 5;
-
private final InvoicePluginDispatcher invoicePluginDispatcher;
private final InvoiceDao dao;
private final GlobalLocker locker;
private final InternalCallContextFactory internalCallContextFactory;
+ private final InvoiceConfig invoiceConfig;
@Inject
- public InvoiceApiHelper(final InvoicePluginDispatcher invoicePluginDispatcher, final InvoiceDao dao, final GlobalLocker locker, final InternalCallContextFactory internalCallContextFactory) {
+ public InvoiceApiHelper(final InvoicePluginDispatcher invoicePluginDispatcher, final InvoiceDao dao, final GlobalLocker locker, final InvoiceConfig invoiceConfig, final InternalCallContextFactory internalCallContextFactory) {
this.invoicePluginDispatcher = invoicePluginDispatcher;
this.dao = dao;
this.locker = locker;
+ this.invoiceConfig = invoiceConfig;
this.internalCallContextFactory = internalCallContextFactory;
}
- public List dispatchToInvoicePluginsAndInsertItems(final UUID accountId, final WithAccountLock withAccountLock, final CallContext context) throws InvoiceApiException {
+ public List dispatchToInvoicePluginsAndInsertItems(final UUID accountId, final boolean isDryRun, final WithAccountLock withAccountLock, final CallContext context) throws InvoiceApiException {
GlobalLock lock = null;
try {
- lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), NB_LOCK_TRY);
+ lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), invoiceConfig.getMaxGlobalLockRetries());
final Iterable invoicesForPlugins = withAccountLock.prepareInvoices();
final List invoiceModelDaos = new LinkedList();
for (final Invoice invoiceForPlugin : invoicesForPlugins) {
// Call plugin
- final List additionalInvoiceItems = invoicePluginDispatcher.getAdditionalInvoiceItems(invoiceForPlugin, context);
+ final List additionalInvoiceItems = invoicePluginDispatcher.getAdditionalInvoiceItems(invoiceForPlugin, isDryRun, context);
invoiceForPlugin.addInvoiceItems(additionalInvoiceItems);
// Transformation to InvoiceModelDao
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
index 47b003471b..ba2227ee1e 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
@@ -21,9 +21,8 @@
import java.util.UUID;
import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
-import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
import org.killbill.billing.util.timezone.DateAndTimeZoneContext;
@@ -69,9 +68,9 @@ public DefaultInvoiceMigrationApi(final AccountInternalApi accountUserApi,
@Override
public UUID createMigrationInvoice(final UUID accountId, final LocalDate targetDate, final BigDecimal balance, final Currency currency, final CallContext context) {
- Account account;
+ ImmutableAccountData account;
try {
- account = accountUserApi.getAccountById(accountId, internalCallContextFactory.createInternalTenantContext(accountId, context));
+ account = accountUserApi.getImmutableAccountDataById(accountId, internalCallContextFactory.createInternalTenantContext(accountId, context));
} catch (AccountApiException e) {
log.warn("Unable to find account for id {}", accountId);
return null;
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
index db51bbb074..b3da4f0b90 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
@@ -94,8 +94,8 @@ public BigDecimal getAccountBalance(final UUID accountId, final InternalTenantCo
}
@Override
- public void notifyOfPayment(final UUID invoiceId, final BigDecimal amount, final Currency currency, final Currency processedCurrency, final UUID paymentId, final DateTime paymentDate, final InternalCallContext context) throws InvoiceApiException {
- final InvoicePayment invoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency);
+ public void notifyOfPayment(final UUID invoiceId, final BigDecimal amount, final Currency currency, final Currency processedCurrency, final UUID paymentId, final DateTime paymentDate, final boolean success, final InternalCallContext context) throws InvoiceApiException {
+ final InvoicePayment invoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, success);
notifyOfPayment(invoicePayment, context);
}
@@ -143,7 +143,7 @@ public Iterable prepareInvoices() throws InvoiceApiException {
return ImmutableList.of(invoice);
}
};
- final List createdInvoiceItems = invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, withAccountLock, callContext);
+ final List createdInvoiceItems = invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, false, withAccountLock, callContext);
return new DefaultInvoicePayment(refund);
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
index 42d8f7c11c..0c2b5b3191 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -35,6 +35,7 @@
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.Currency;
@@ -205,9 +206,9 @@ public Invoice triggerInvoiceGeneration(final UUID accountId, @Nullable final Lo
final CallContext context) throws InvoiceApiException {
final InternalCallContext internalContext = internalCallContextFactory.createInternalCallContext(accountId, context);
- final Account account;
+ final ImmutableAccountData account;
try {
- account = accountUserApi.getAccountById(accountId, internalContext);
+ account = accountUserApi.getImmutableAccountDataById(accountId, internalContext);
} catch (final AccountApiException e) {
throw new InvoiceApiException(e, ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_ID, e.toString());
}
@@ -308,7 +309,7 @@ public Iterable prepareInvoices() throws InvoiceApiException {
}
};
- return invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, withAccountLock, context);
+ return invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, false, withAccountLock, context);
}
@Override
@@ -364,7 +365,7 @@ public List prepareInvoices() throws InvoiceApiException {
}
};
- final List creditInvoiceItems = invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, withAccountLock, context);
+ final List creditInvoiceItems = invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, false, withAccountLock, context);
Preconditions.checkState(creditInvoiceItems.size() == 1, "Should have created a single credit invoice item: " + creditInvoiceItems);
return creditInvoiceItems.get(0);
@@ -400,7 +401,7 @@ public Iterable prepareInvoices() throws InvoiceApiException {
}
};
- final List adjustmentInvoiceItems = invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, withAccountLock, context);
+ final List adjustmentInvoiceItems = invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, false, withAccountLock, context);
Preconditions.checkState(adjustmentInvoiceItems.size() == 1, "Should have created a single adjustment item: " + adjustmentInvoiceItems);
return adjustmentInvoiceItems.get(0);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java b/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
index 1437745439..eceab78935 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
@@ -170,6 +170,9 @@ public static BigDecimal computeInvoiceAmountPaid(final Currency currency, @Null
}
for (final InvoicePayment invoicePayment : invoicePayments) {
+ if (!invoicePayment.isSuccess()) {
+ continue;
+ }
if (InvoicePaymentType.ATTEMPT.equals(invoicePayment.getType())) {
amountPaid = amountPaid.add(invoicePayment.getAmount());
}
@@ -185,6 +188,9 @@ public static BigDecimal computeInvoiceAmountRefunded(final Currency currency, @
}
for (final InvoicePayment invoicePayment : invoicePayments) {
+ if (!invoicePayment.isSuccess()) {
+ continue;
+ }
if (InvoicePaymentType.REFUND.equals(invoicePayment.getType()) ||
InvoicePaymentType.CHARGED_BACK.equals(invoicePayment.getType())) {
amountRefunded = amountRefunded.add(invoicePayment.getAmount());
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java
index 866d2484f2..00df90075d 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java
@@ -79,15 +79,6 @@ public InvoiceItemModelDao computeCBAComplexity(final InvoiceModelDao invoice, f
}
}
- // We expect a clean up to date invoice, with all the items except the CBA, that we will compute in that method
- public void addCBAComplexityFromTransaction(final InvoiceModelDao invoice, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
- final InvoiceItemModelDao cbaItem = computeCBAComplexity(invoice, entitySqlDaoWrapperFactory, context);
- if (cbaItem != null) {
- final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
- transInvoiceItemDao.create(cbaItem, context);
- }
- }
-
// We let the code below rehydrate the invoice before we can add the CBA item
public void addCBAComplexityFromTransaction(final UUID invoiceId, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
@@ -97,6 +88,17 @@ public void addCBAComplexityFromTransaction(final UUID invoiceId, final EntitySq
addCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
}
+ // We expect a clean up to date invoice, with all the items except the CBA, that we will compute in that method
+ public void addCBAComplexityFromTransaction(final InvoiceModelDao invoice, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
+ final InvoiceItemModelDao cbaItem = computeCBAComplexity(invoice, entitySqlDaoWrapperFactory, context);
+ if (cbaItem != null) {
+ final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
+ transInvoiceItemDao.create(cbaItem, context);
+ }
+ List invoiceItemModelDaos = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
+ useExistingCBAFromTransaction(invoiceItemModelDaos, entitySqlDaoWrapperFactory, context);
+ }
+
public void addCBAComplexityFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
List invoiceItemModelDaos = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index 655c5da6d8..0f4cdc059e 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -462,7 +462,7 @@ public boolean apply(final InvoicePaymentModelDao input) {
final InvoicePaymentModelDao refund = new InvoicePaymentModelDao(UUIDs.randomUUID(), context.getCreatedDate(), InvoicePaymentType.REFUND,
payment.getInvoiceId(), paymentId,
context.getCreatedDate(), requestedPositiveAmount.negate(),
- payment.getCurrency(), payment.getProcessedCurrency(), transactionExternalKey, payment.getId());
+ payment.getCurrency(), payment.getProcessedCurrency(), transactionExternalKey, payment.getId(), true);
transactional.create(refund, context);
// Retrieve invoice after the Refund
@@ -548,7 +548,7 @@ public boolean apply(final InvoicePaymentModelDao input) {
final InvoicePaymentModelDao chargeBack = new InvoicePaymentModelDao(UUIDs.randomUUID(), context.getCreatedDate(), InvoicePaymentType.CHARGED_BACK,
payment.getInvoiceId(), payment.getPaymentId(), context.getCreatedDate(),
requestedChargedBackAmount.negate(), payment.getCurrency(), payment.getProcessedCurrency(),
- null, payment.getId());
+ null, payment.getId(), true);
transactional.create(chargeBack, context);
// Notify the bus since the balance of the invoice changed
@@ -664,6 +664,8 @@ public boolean apply(final InvoicePaymentModelDao input) {
}).orNull();
if (existingAttempt == null) {
transactional.create(invoicePayment, context);
+ } else if (!existingAttempt.getSuccess() && invoicePayment.getSuccess()) {
+ transactional.updateAttempt(existingAttempt.getRecordId(), invoicePayment.getPaymentDate().toDate(), invoicePayment.getAmount(), invoicePayment.getCurrency(), invoicePayment.getProcessedCurrency(), context);
}
return null;
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentModelDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentModelDao.java
index 20a7c811ff..e05448f715 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentModelDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentModelDao.java
@@ -40,12 +40,13 @@ public class InvoicePaymentModelDao extends EntityModelDaoBase implements Entity
private Currency processedCurrency;
private String paymentCookieId;
private UUID linkedInvoicePaymentId;
+ private Boolean success;
public InvoicePaymentModelDao() { /* For the DAO mapper */ }
public InvoicePaymentModelDao(final UUID id, final DateTime createdDate, final InvoicePaymentType type, final UUID invoiceId,
final UUID paymentId, final DateTime paymentDate, final BigDecimal amount, final Currency currency,
- final Currency processedCurrency, final String paymentCookieId, final UUID linkedInvoicePaymentId) {
+ final Currency processedCurrency, final String paymentCookieId, final UUID linkedInvoicePaymentId, final Boolean success) {
super(id, createdDate, createdDate);
this.type = type;
this.invoiceId = invoiceId;
@@ -56,12 +57,13 @@ public InvoicePaymentModelDao(final UUID id, final DateTime createdDate, final I
this.processedCurrency = processedCurrency;
this.paymentCookieId = paymentCookieId;
this.linkedInvoicePaymentId = linkedInvoicePaymentId;
+ this.success = success;
}
public InvoicePaymentModelDao(final InvoicePayment invoicePayment) {
this(invoicePayment.getId(), invoicePayment.getCreatedDate(), invoicePayment.getType(), invoicePayment.getInvoiceId(), invoicePayment.getPaymentId(),
invoicePayment.getPaymentDate(), invoicePayment.getAmount(), invoicePayment.getCurrency(), invoicePayment.getProcessedCurrency(), invoicePayment.getPaymentCookieId(),
- invoicePayment.getLinkedInvoicePaymentId());
+ invoicePayment.getLinkedInvoicePaymentId(), invoicePayment.isSuccess());
}
public InvoicePaymentType getType() {
@@ -136,6 +138,14 @@ public void setLinkedInvoicePaymentId(final UUID linkedInvoicePaymentId) {
this.linkedInvoicePaymentId = linkedInvoicePaymentId;
}
+ public Boolean getSuccess() {
+ return success;
+ }
+
+ public void setSuccess(final Boolean success) {
+ this.success = success;
+ }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
index 626dda4de5..279aea38b1 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
@@ -19,16 +19,19 @@
package org.killbill.billing.invoice.dao;
import java.math.BigDecimal;
+import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.invoice.api.InvoicePayment;
import org.killbill.billing.util.entity.dao.EntitySqlDao;
import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
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;
@EntitySqlDaoStringTemplate
public interface InvoicePaymentSqlDao extends EntitySqlDao {
@@ -64,4 +67,14 @@ List getChargeBacksByAccountId(@Bind("accountId") final
@SqlQuery
List getChargebacksByPaymentId(@Bind("paymentId") final String paymentId,
@BindBean final InternalTenantContext context);
+
+
+
+ @SqlUpdate
+ void updateAttempt(@Bind("recordId") Long recordId,
+ @Bind("paymentDate") final Date paymentDate,
+ @Bind("amount") final BigDecimal amount,
+ @Bind("currency") final Currency currency,
+ @Bind("processedCurrency") final Currency processedCurrency,
+ @BindBean final InternalTenantContext context);
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
index 2d95f1fa1a..76843fd121 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
@@ -17,7 +17,7 @@
package org.killbill.billing.invoice.generator;
import org.joda.time.LocalDate;
-
+import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.BillingPeriod;
import com.google.common.annotations.VisibleForTesting;
@@ -29,17 +29,25 @@ public class BillingIntervalDetail {
private final LocalDate targetDate;
private final int billingCycleDay;
private final BillingPeriod billingPeriod;
-
+ private final BillingMode billingMode;
+ // First date after the startDate aligned with the BCD
private LocalDate firstBillingCycleDate;
+ // Date up to which we should bill
private LocalDate effectiveEndDate;
private LocalDate lastBillingCycleDate;
- public BillingIntervalDetail(final LocalDate startDate, final LocalDate endDate, final LocalDate targetDate, final int billingCycleDay, final BillingPeriod billingPeriod) {
+ public BillingIntervalDetail(final LocalDate startDate,
+ final LocalDate endDate,
+ final LocalDate targetDate,
+ final int billingCycleDay,
+ final BillingPeriod billingPeriod,
+ final BillingMode billingMode) {
this.startDate = startDate;
this.endDate = endDate;
this.targetDate = targetDate;
this.billingCycleDay = billingCycleDay;
this.billingPeriod = billingPeriod;
+ this.billingMode = billingMode;
computeAll();
}
@@ -61,6 +69,19 @@ public LocalDate getLastBillingCycleDate() {
return lastBillingCycleDate;
}
+ public LocalDate getNextBillingCycleDate() {
+ final int numberOfMonthsInPeriod = billingPeriod.getNumberOfMonths();
+ final LocalDate proposedDate = lastBillingCycleDate != null ? lastBillingCycleDate.plusMonths(numberOfMonthsInPeriod) : firstBillingCycleDate;
+ final LocalDate nextBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay);
+ return nextBillingCycleDate;
+ }
+
+
+ public boolean hasSomethingToBill() {
+ return effectiveEndDate != null /* IN_ARREAR mode prior we have reached our firstBillingCycleDate */ &&
+ (endDate == null || endDate.isAfter(startDate)); /* When there is an endDate, it should be > startDate since we don't bill for less than a day */
+ }
+
private void computeAll() {
calculateFirstBillingCycleDate();
calculateEffectiveEndDate();
@@ -87,6 +108,47 @@ void calculateFirstBillingCycleDate() {
}
private void calculateEffectiveEndDate() {
+ if (billingMode == BillingMode.IN_ADVANCE) {
+ calculateInAdvanceEffectiveEndDate();
+ } else {
+ calculateInArrearEffectiveEndDate();
+ }
+ }
+
+ private void calculateInArrearEffectiveEndDate() {
+ if (targetDate.isBefore(firstBillingCycleDate)) {
+ // Nothing to bill for, hasSomethingToBill will return false
+ effectiveEndDate = null;
+ return;
+ }
+
+ if (endDate != null && endDate.isBefore(firstBillingCycleDate)) {
+ effectiveEndDate = endDate;
+ return;
+ }
+
+ final int numberOfMonthsInPeriod = billingPeriod.getNumberOfMonths();
+ int numberOfPeriods = 0;
+ LocalDate proposedDate = firstBillingCycleDate;
+ LocalDate nextProposedDate = proposedDate.plusMonths(numberOfPeriods * numberOfMonthsInPeriod);
+
+
+ while (!nextProposedDate.isAfter(targetDate)) {
+ proposedDate = nextProposedDate;
+ nextProposedDate = firstBillingCycleDate.plusMonths(numberOfPeriods * numberOfMonthsInPeriod);
+ numberOfPeriods += 1;
+ }
+ proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay);
+
+ // We honor the endDate as long as it does not go beyond our targetDate (by construction this cannot be after the nextProposedDate neither.
+ if (endDate != null && !endDate.isAfter(targetDate)) {
+ effectiveEndDate = endDate;
+ } else {
+ effectiveEndDate = proposedDate;
+ }
+ }
+
+ private void calculateInAdvanceEffectiveEndDate() {
// We have an endDate and the targetDate is greater or equal to our endDate => return it
if (endDate != null && !targetDate.isBefore(endDate)) {
@@ -117,9 +179,14 @@ private void calculateEffectiveEndDate() {
}
}
-
private void calculateLastBillingCycleDate() {
+ // IN_ARREAR cases
+ if (effectiveEndDate == null || effectiveEndDate.compareTo(firstBillingCycleDate) < 0 ) {
+ lastBillingCycleDate = null;
+ return;
+ }
+
// Start from firstBillingCycleDate and billingPeriod until we pass the effectiveEndDate
LocalDate proposedDate = firstBillingCycleDate;
int numberOfPeriods = 0;
@@ -134,13 +201,12 @@ private void calculateLastBillingCycleDate() {
if (proposedDate.isBefore(firstBillingCycleDate)) {
// Make sure not to go too far in the past
- lastBillingCycleDate = firstBillingCycleDate;
+ lastBillingCycleDate = firstBillingCycleDate;
} else {
- lastBillingCycleDate = proposedDate;
+ lastBillingCycleDate = proposedDate;
}
}
-
//
// We start from a billCycleDate
//
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
index 1bc6c58ce0..c9a290ae4c 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
@@ -18,11 +18,7 @@
package org.killbill.billing.invoice.generator;
-import java.math.BigDecimal;
-import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -32,71 +28,47 @@
import org.joda.time.LocalDate;
import org.joda.time.Months;
import org.killbill.billing.ErrorCode;
-import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.ImmutableAccountData;
import org.killbill.billing.callcontext.InternalCallContext;
-import org.killbill.billing.catalog.api.BillingMode;
-import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Currency;
-import org.killbill.billing.catalog.api.Usage;
-import org.killbill.billing.catalog.api.UsageType;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
-import org.killbill.billing.invoice.api.InvoiceItemType;
-import org.killbill.billing.invoice.model.BillingModeGenerator;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
import org.killbill.billing.invoice.model.DefaultInvoice;
-import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
-import org.killbill.billing.invoice.model.InAdvanceBillingMode;
-import org.killbill.billing.invoice.model.InvalidDateSequenceException;
-import org.killbill.billing.invoice.model.RecurringInvoiceItem;
-import org.killbill.billing.invoice.model.RecurringInvoiceItemData;
-import org.killbill.billing.invoice.tree.AccountItemTree;
-import org.killbill.billing.invoice.usage.RawUsageOptimizer;
-import org.killbill.billing.invoice.usage.RawUsageOptimizer.RawUsageOptimizerResult;
-import org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear;
-import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.junction.BillingEventSet;
-import org.killbill.billing.usage.RawUsage;
import org.killbill.billing.util.config.InvoiceConfig;
-import org.killbill.billing.util.currency.KillBillMoney;
import org.killbill.clock.Clock;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
import com.google.inject.Inject;
public class DefaultInvoiceGenerator implements InvoiceGenerator {
- private static final Logger log = LoggerFactory.getLogger(DefaultInvoiceGenerator.class);
-
private final Clock clock;
private final InvoiceConfig config;
- private final RawUsageOptimizer rawUsageOptimizer;
+
+ private final FixedAndRecurringInvoiceItemGenerator recurringInvoiceItemGenerator;
+ private final UsageInvoiceItemGenerator usageInvoiceItemGenerator;
@Inject
- public DefaultInvoiceGenerator(final Clock clock, final InvoiceConfig config, final RawUsageOptimizer rawUsageOptimizer) {
+ public DefaultInvoiceGenerator(final Clock clock, final InvoiceConfig config, final FixedAndRecurringInvoiceItemGenerator recurringInvoiceItemGenerator, final UsageInvoiceItemGenerator usageInvoiceItemGenerator) {
this.clock = clock;
this.config = config;
- this.rawUsageOptimizer = rawUsageOptimizer;
+ this.recurringInvoiceItemGenerator = recurringInvoiceItemGenerator;
+ this.usageInvoiceItemGenerator = usageInvoiceItemGenerator;
}
/*
* adjusts target date to the maximum invoice target date, if future invoices exist
*/
@Override
- public Invoice generateInvoice(final Account account, @Nullable final BillingEventSet events,
- @Nullable final List existingInvoices,
- final LocalDate targetDate,
- final Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException {
+ public InvoiceWithMetadata generateInvoice(final ImmutableAccountData account, @Nullable final BillingEventSet events,
+ @Nullable final List existingInvoices,
+ final LocalDate targetDate,
+ final Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException {
if ((events == null) || (events.size() == 0) || events.isAccountAutoInvoiceOff()) {
- return null;
+ return new InvoiceWithMetadata(null, ImmutableMap.of());
}
validateTargetDate(targetDate);
@@ -104,132 +76,16 @@ public Invoice generateInvoice(final Account account, @Nullable final BillingEve
final Invoice invoice = new DefaultInvoice(account.getId(), new LocalDate(clock.getUTCNow(), account.getTimeZone()), adjustedTargetDate, targetCurrency);
final UUID invoiceId = invoice.getId();
+ final Map perSubscriptionFutureNotificationDates = new HashMap();
- final List inAdvanceItems = generateInAdvanceInvoiceItems(account.getId(), invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency);
- invoice.addInvoiceItems(inAdvanceItems);
+ final List fixedAndRecurringItems = recurringInvoiceItemGenerator.generateItems(account, invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates, context);
+ invoice.addInvoiceItems(fixedAndRecurringItems);
- final List usageItems = generateUsageConsumableInArrearItems(account, invoiceId, events, existingInvoices, targetDate, context);
+ final List usageItems = usageInvoiceItemGenerator.generateItems(account, invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates, context);
invoice.addInvoiceItems(usageItems);
- return invoice.getInvoiceItems().size() != 0 ? invoice : null;
- }
-
- private List generateUsageConsumableInArrearItems(final Account account,
- final UUID invoiceId, final BillingEventSet eventSet,
- @Nullable final List existingInvoices, final LocalDate targetDate,
- final InternalCallContext internalCallContext) throws InvoiceApiException {
-
- final Map> perSubscriptionConsumableInArrearUsageItems = extractPerSubscriptionExistingConsumableInArrearUsageItems(eventSet.getUsages(), existingInvoices);
- try {
- final List items = Lists.newArrayList();
- final Iterator events = eventSet.iterator();
-
- RawUsageOptimizerResult rawUsageOptimizerResult = null;
- List curEvents = Lists.newArrayList();
- UUID curSubscriptionId = null;
- while (events.hasNext()) {
- final BillingEvent event = events.next();
- // Skip events that are posterior to the targetDate
- final LocalDate eventLocalEffectiveDate = new LocalDate(event.getEffectiveDate(), event.getAccount().getTimeZone());
- if (eventLocalEffectiveDate.isAfter(targetDate)) {
- continue;
- }
-
- // Optimize to do the usage query only once after we know there are indeed some usage items
- if (rawUsageOptimizerResult == null &&
- Iterables.any(event.getUsages(), new Predicate() {
- @Override
- public boolean apply(@Nullable final Usage input) {
- return (input.getUsageType() == UsageType.CONSUMABLE &&
- input.getBillingMode() == BillingMode.IN_ARREAR);
- }
- })) {
- rawUsageOptimizerResult = rawUsageOptimizer.getConsumableInArrearUsage(new LocalDate(event.getEffectiveDate(), account.getTimeZone()), targetDate, Iterables.concat(perSubscriptionConsumableInArrearUsageItems.values()), eventSet.getUsages(), internalCallContext);
- }
-
- // None of the billing events report any usage (CONSUMABLE/IN_ARREAR) sections
- if (rawUsageOptimizerResult == null) {
- continue;
- }
- final UUID subscriptionId = event.getSubscription().getId();
- if (curSubscriptionId != null && !curSubscriptionId.equals(subscriptionId)) {
- final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate());
- final List consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId);
- items.addAll(subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.of()));
- curEvents = Lists.newArrayList();
- }
- curSubscriptionId = subscriptionId;
- curEvents.add(event);
- }
- if (curSubscriptionId != null) {
- final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate());
- final List consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId);
- items.addAll(subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.of()));
- }
- return items;
-
- } catch (CatalogApiException e) {
- throw new InvoiceApiException(e);
- }
- }
-
- private Map> extractPerSubscriptionExistingConsumableInArrearUsageItems(final Map knownUsage, @Nullable final List existingInvoices) {
-
- if (existingInvoices == null || existingInvoices.isEmpty()) {
- return ImmutableMap.of();
- }
-
- final Map> result = new HashMap>();
- final Iterable usageConsumableInArrearItems = Iterables.concat(Iterables.transform(existingInvoices, new Function>() {
- @Override
- public Iterable apply(final Invoice input) {
-
- return Iterables.filter(input.getInvoiceItems(), new Predicate() {
- @Override
- public boolean apply(final InvoiceItem input) {
- if (input.getInvoiceItemType() == InvoiceItemType.USAGE) {
- final Usage usage = knownUsage.get(input.getUsageName());
- return usage.getUsageType() == UsageType.CONSUMABLE && usage.getBillingMode() == BillingMode.IN_ARREAR;
- }
- return false;
- }
- });
- }
- }));
-
- for (InvoiceItem cur : usageConsumableInArrearItems) {
- List perSubscriptionUsageItems = result.get(cur.getSubscriptionId());
- if (perSubscriptionUsageItems == null) {
- perSubscriptionUsageItems = new LinkedList();
- result.put(cur.getSubscriptionId(), perSubscriptionUsageItems);
- }
- perSubscriptionUsageItems.add(cur);
- }
- return result;
- }
-
- private List generateInAdvanceInvoiceItems(final UUID accountId, final UUID invoiceId, final BillingEventSet eventSet,
- @Nullable final List existingInvoices, final LocalDate targetDate,
- final Currency targetCurrency) throws InvoiceApiException {
- final AccountItemTree accountItemTree = new AccountItemTree(accountId, invoiceId);
- if (existingInvoices != null) {
- for (final Invoice invoice : existingInvoices) {
- for (final InvoiceItem item : invoice.getInvoiceItems()) {
- if (item.getSubscriptionId() == null || // Always include migration invoices, credits, external charges etc.
- !eventSet.getSubscriptionIdsWithAutoInvoiceOff()
- .contains(item.getSubscriptionId())) { //don't add items with auto_invoice_off tag
- accountItemTree.addExistingItem(item);
- }
- }
- }
- }
-
- // Generate list of proposed invoice items based on billing events from junction-- proposed items are ALL items since beginning of time
- final List proposedItems = generateInAdvanceInvoiceItems(invoiceId, accountId, eventSet, targetDate, targetCurrency);
-
- accountItemTree.mergeWithProposedItems(proposedItems);
- return accountItemTree.getResultingItemList();
+ return new InvoiceWithMetadata(invoice.getInvoiceItems().isEmpty() ? null : invoice, perSubscriptionFutureNotificationDates);
}
private void validateTargetDate(final LocalDate targetDate) throws InvoiceApiException {
@@ -255,125 +111,4 @@ private LocalDate adjustTargetDate(final List existingInvoices, final L
return maxDate;
}
- private List generateInAdvanceInvoiceItems(final UUID invoiceId, final UUID accountId, final BillingEventSet events,
- final LocalDate targetDate, final Currency currency) throws InvoiceApiException {
- final List items = new ArrayList();
-
- if (events.size() == 0) {
- return items;
- }
-
- // Pretty-print the generated invoice items from the junction events
- final StringBuilder logStringBuilder = new StringBuilder("Proposed Invoice items for invoiceId ")
- .append(invoiceId)
- .append(" and accountId ")
- .append(accountId);
-
- final Iterator eventIt = events.iterator();
- BillingEvent nextEvent = eventIt.next();
- while (eventIt.hasNext()) {
- final BillingEvent thisEvent = nextEvent;
- nextEvent = eventIt.next();
- if (!events.getSubscriptionIdsWithAutoInvoiceOff().
- contains(thisEvent.getSubscription().getId())) { // don't consider events for subscriptions that have auto_invoice_off
- final BillingEvent adjustedNextEvent = (thisEvent.getSubscription().getId() == nextEvent.getSubscription().getId()) ? nextEvent : null;
- items.addAll(processInAdvanceEvents(invoiceId, accountId, thisEvent, adjustedNextEvent, targetDate, currency, logStringBuilder));
- }
- }
- items.addAll(processInAdvanceEvents(invoiceId, accountId, nextEvent, null, targetDate, currency, logStringBuilder));
-
- log.info(logStringBuilder.toString());
-
- return items;
- }
-
- // Turn a set of events into a list of invoice items. Note that the dates on the invoice items will be rounded (granularity of a day)
- private List processInAdvanceEvents(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent, @Nullable final BillingEvent nextEvent,
- final LocalDate targetDate, final Currency currency,
- final StringBuilder logStringBuilder) throws InvoiceApiException {
- final List items = new ArrayList();
-
- // Handle fixed price items
- final InvoiceItem fixedPriceInvoiceItem = generateFixedPriceItem(invoiceId, accountId, thisEvent, targetDate, currency);
- if (fixedPriceInvoiceItem != null) {
- items.add(fixedPriceInvoiceItem);
- }
-
- // Handle recurring items
- final BillingPeriod billingPeriod = thisEvent.getBillingPeriod();
- if (billingPeriod != BillingPeriod.NO_BILLING_PERIOD) {
- final BillingModeGenerator billingModeGenerator = instantiateBillingMode(thisEvent.getBillingMode());
- final LocalDate startDate = new LocalDate(thisEvent.getEffectiveDate(), thisEvent.getTimeZone());
-
- if (!startDate.isAfter(targetDate)) {
- final LocalDate endDate = (nextEvent == null) ? null : new LocalDate(nextEvent.getEffectiveDate(), nextEvent.getTimeZone());
-
- final int billCycleDayLocal = thisEvent.getBillCycleDayLocal();
-
- final List itemData;
- try {
- itemData = billingModeGenerator.generateInvoiceItemData(startDate, endDate, targetDate, billCycleDayLocal, billingPeriod);
- } catch (InvalidDateSequenceException e) {
- throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_DATE_SEQUENCE, startDate, endDate, targetDate);
- }
-
- for (final RecurringInvoiceItemData itemDatum : itemData) {
- final BigDecimal rate = thisEvent.getRecurringPrice();
-
- if (rate != null) {
- final BigDecimal amount = KillBillMoney.of(itemDatum.getNumberOfCycles().multiply(rate), currency);
-
- final RecurringInvoiceItem recurringItem = new RecurringInvoiceItem(invoiceId,
- accountId,
- thisEvent.getSubscription().getBundleId(),
- thisEvent.getSubscription().getId(),
- thisEvent.getPlan().getName(),
- thisEvent.getPlanPhase().getName(),
- itemDatum.getStartDate(), itemDatum.getEndDate(),
- amount, rate, currency);
- items.add(recurringItem);
- }
- }
- }
- }
-
- // For debugging purposes
- logStringBuilder.append("\n")
- .append(thisEvent);
- for (final InvoiceItem item : items) {
- logStringBuilder.append("\n\t")
- .append(item);
- }
-
- return items;
- }
-
- private BillingModeGenerator instantiateBillingMode(final BillingMode billingMode) {
- switch (billingMode) {
- case IN_ADVANCE:
- return new InAdvanceBillingMode();
- default:
- throw new UnsupportedOperationException();
- }
- }
-
- InvoiceItem generateFixedPriceItem(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent,
- final LocalDate targetDate, final Currency currency) {
- final LocalDate roundedStartDate = new LocalDate(thisEvent.getEffectiveDate(), thisEvent.getTimeZone());
-
- if (roundedStartDate.isAfter(targetDate)) {
- return null;
- } else {
- final BigDecimal fixedPrice = thisEvent.getFixedPrice();
-
- if (fixedPrice != null) {
- return new FixedPriceInvoiceItem(invoiceId, accountId, thisEvent.getSubscription().getBundleId(),
- thisEvent.getSubscription().getId(),
- thisEvent.getPlan().getName(), thisEvent.getPlanPhase().getName(),
- roundedStartDate, fixedPrice, currency);
- } else {
- return null;
- }
- }
- }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
new file mode 100644
index 0000000000..940f7465c5
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
@@ -0,0 +1,336 @@
+/*
+ * 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.invoice.generator;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.ImmutableAccountData;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.BillingMode;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
+import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.model.RecurringInvoiceItem;
+import org.killbill.billing.invoice.model.RecurringInvoiceItemData;
+import org.killbill.billing.invoice.model.RecurringInvoiceItemDataWithNextBillingCycleDate;
+import org.killbill.billing.invoice.tree.AccountItemTree;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingEventSet;
+import org.killbill.billing.util.currency.KillBillMoney;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Inject;
+
+import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateNumberOfWholeBillingPeriods;
+import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateProRationAfterLastBillingCycleDate;
+import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateProRationBeforeFirstBillingPeriod;
+
+public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator {
+
+ private static final Logger log = LoggerFactory.getLogger(FixedAndRecurringInvoiceItemGenerator.class);
+
+ @Inject
+ public FixedAndRecurringInvoiceItemGenerator() {
+ }
+
+ public List generateItems(final ImmutableAccountData account, final UUID invoiceId, final BillingEventSet eventSet,
+ @Nullable final List existingInvoices, final LocalDate targetDate,
+ final Currency targetCurrency, Map perSubscriptionFutureNotificationDate,
+ final InternalCallContext internalCallContext) throws InvoiceApiException {
+ final AccountItemTree accountItemTree = new AccountItemTree(account.getId(), invoiceId);
+ if (existingInvoices != null) {
+ for (final Invoice invoice : existingInvoices) {
+ for (final InvoiceItem item : invoice.getInvoiceItems()) {
+ if (item.getSubscriptionId() == null || // Always include migration invoices, credits, external charges etc.
+ !eventSet.getSubscriptionIdsWithAutoInvoiceOff()
+ .contains(item.getSubscriptionId())) { //don't add items with auto_invoice_off tag
+ accountItemTree.addExistingItem(item);
+ }
+ }
+ }
+ }
+
+ // Generate list of proposed invoice items based on billing events from junction-- proposed items are ALL items since beginning of time
+ final List proposedItems = new ArrayList();
+ processRecurringBillingEvents(invoiceId, account.getId(), eventSet, targetDate, targetCurrency, proposedItems, perSubscriptionFutureNotificationDate);
+ processFixedBillingEvents(invoiceId, account.getId(), eventSet, targetDate, targetCurrency, proposedItems);
+
+ accountItemTree.mergeWithProposedItems(proposedItems);
+ return accountItemTree.getResultingItemList();
+ }
+
+ private List processRecurringBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events,
+ final LocalDate targetDate, final Currency currency, final List proposedItems,
+ final Map perSubscriptionFutureNotificationDate) throws InvoiceApiException {
+
+ if (events.size() == 0) {
+ return proposedItems;
+ }
+
+ // Pretty-print the generated invoice items from the junction events
+ final StringBuilder logStringBuilder = new StringBuilder("Proposed Invoice items for invoiceId ")
+ .append(invoiceId)
+ .append(" and accountId ")
+ .append(accountId);
+
+ final Iterator eventIt = events.iterator();
+ BillingEvent nextEvent = eventIt.next();
+ while (eventIt.hasNext()) {
+ final BillingEvent thisEvent = nextEvent;
+ nextEvent = eventIt.next();
+ if (!events.getSubscriptionIdsWithAutoInvoiceOff().
+ contains(thisEvent.getSubscription().getId())) { // don't consider events for subscriptions that have auto_invoice_off
+ final BillingEvent adjustedNextEvent = (thisEvent.getSubscription().getId() == nextEvent.getSubscription().getId()) ? nextEvent : null;
+ final List newProposedItems = processRecurringEvent(invoiceId, accountId, thisEvent, adjustedNextEvent, targetDate, currency, logStringBuilder, events.getRecurringBillingMode(), perSubscriptionFutureNotificationDate);
+ proposedItems.addAll(newProposedItems);
+ }
+ }
+ final List