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 newProposedItems = processRecurringEvent(invoiceId, accountId, nextEvent, null, targetDate, currency, logStringBuilder, events.getRecurringBillingMode(), perSubscriptionFutureNotificationDate); + proposedItems.addAll(newProposedItems); + + log.info(logStringBuilder.toString()); + + return proposedItems; + } + + private List processFixedBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events, final LocalDate targetDate, final Currency currency, final List proposedItems) { + final Iterator eventIt = events.iterator(); + while (eventIt.hasNext()) { + final BillingEvent thisEvent = eventIt.next(); + + final InvoiceItem fixedPriceInvoiceItem = generateFixedPriceItem(invoiceId, accountId, thisEvent, targetDate, currency); + if (fixedPriceInvoiceItem != null) { + proposedItems.add(fixedPriceInvoiceItem); + } + } + return proposedItems; + } + + // 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 processRecurringEvent(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent, @Nullable final BillingEvent nextEvent, + final LocalDate targetDate, final Currency currency, + final StringBuilder logStringBuilder, final BillingMode billingMode, + final Map perSubscriptionFutureNotificationDate) throws InvoiceApiException { + final List items = new ArrayList(); + + // Handle recurring items + final BillingPeriod billingPeriod = thisEvent.getBillingPeriod(); + if (billingPeriod != BillingPeriod.NO_BILLING_PERIOD) { + 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 RecurringInvoiceItemDataWithNextBillingCycleDate itemDataWithNextBillingCycleDate; + try { + itemDataWithNextBillingCycleDate = generateInvoiceItemData(startDate, endDate, targetDate, billCycleDayLocal, billingPeriod, billingMode); + } catch (InvalidDateSequenceException e) { + throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_DATE_SEQUENCE, startDate, endDate, targetDate); + } + + for (final RecurringInvoiceItemData itemDatum : itemDataWithNextBillingCycleDate.getItemData()) { + 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); + } + } + updatePerSubscriptionNextNotificationDate(thisEvent.getSubscription().getId(), itemDataWithNextBillingCycleDate.getNextBillingCycleDate(), items, billingMode, perSubscriptionFutureNotificationDate); + } + } + + // For debugging purposes + logStringBuilder.append("\n") + .append(thisEvent); + for (final InvoiceItem item : items) { + logStringBuilder.append("\n\t") + .append(item); + } + return items; + } + + private void updatePerSubscriptionNextNotificationDate(final UUID subscriptionId, final LocalDate nextBillingCycleDate, final List newProposedItems, final BillingMode billingMode, final Map perSubscriptionFutureNotificationDates) { + + LocalDate nextNotificationDate = null; + switch (billingMode) { + case IN_ADVANCE: + for (final InvoiceItem item : newProposedItems) { + if ((item.getEndDate() != null) && + (item.getAmount() == null || + item.getAmount().compareTo(BigDecimal.ZERO) >= 0)) { + if (nextNotificationDate == null) { + nextNotificationDate = item.getEndDate(); + } else { + nextNotificationDate = nextNotificationDate.compareTo(item.getEndDate()) > 0 ? nextNotificationDate : item.getEndDate(); + } + } + } + break; + case IN_ARREAR: + nextNotificationDate = nextBillingCycleDate; + break; + default: + throw new IllegalStateException("Unrecognized billing mode " + billingMode); + } + if (nextNotificationDate != null) { + SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates.get(subscriptionId); + if (subscriptionFutureNotificationDates == null) { + subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates(billingMode); + perSubscriptionFutureNotificationDates.put(subscriptionId, subscriptionFutureNotificationDates); + } + subscriptionFutureNotificationDates.updateNextRecurringDateIfRequired(nextNotificationDate); + + } + } + + public RecurringInvoiceItemDataWithNextBillingCycleDate generateInvoiceItemData(final LocalDate startDate, @Nullable final LocalDate endDate, + final LocalDate targetDate, + final int billingCycleDayLocal, + final BillingPeriod billingPeriod, + final BillingMode billingMode) throws InvalidDateSequenceException { + if (endDate != null && endDate.isBefore(startDate)) { + throw new InvalidDateSequenceException(); + } + if (targetDate.isBefore(startDate)) { + throw new InvalidDateSequenceException(); + } + + final List results = new ArrayList(); + + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(startDate, endDate, targetDate, billingCycleDayLocal, billingPeriod, billingMode); + + // We are not billing for less than a day + if (!billingIntervalDetail.hasSomethingToBill()) { + return new RecurringInvoiceItemDataWithNextBillingCycleDate(results, billingIntervalDetail); + } + // + // If there is an endDate and that endDate is before our first coming firstBillingCycleDate, all we have to do + // is to charge for that period + // + if (endDate != null && !endDate.isAfter(billingIntervalDetail.getFirstBillingCycleDate())) { + final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, endDate, billingPeriod); + final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, endDate, leadingProRationPeriods); + results.add(itemData); + return new RecurringInvoiceItemDataWithNextBillingCycleDate(results, billingIntervalDetail); + } + + // + // Leading proration if + // i) The first firstBillingCycleDate is strictly after our start date AND + // ii) The endDate is is not null and is strictly after our firstBillingCycleDate (previous check) + // + if (billingIntervalDetail.getFirstBillingCycleDate().isAfter(startDate)) { + final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, billingIntervalDetail.getFirstBillingCycleDate(), billingPeriod); + if (leadingProRationPeriods != null && leadingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) { + // Not common - add info in the logs for debugging purposes + final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, billingIntervalDetail.getFirstBillingCycleDate(), leadingProRationPeriods); + log.info("Adding pro-ration: {}", itemData); + results.add(itemData); + } + } + + // + // Calculate the effectiveEndDate from the firstBillingCycleDate: + // - If endDate != null and targetDate is after endDate => this is the endDate and will lead to a trailing pro-ration + // - If not, this is the last billingCycleDate calculation right after the targetDate + // + final LocalDate effectiveEndDate = billingIntervalDetail.getEffectiveEndDate(); + + // + // Based on what we calculated previously, code recompute one more time the numberOfWholeBillingPeriods + // + final LocalDate lastBillingCycleDate = billingIntervalDetail.getLastBillingCycleDate(); + final int numberOfWholeBillingPeriods = calculateNumberOfWholeBillingPeriods(billingIntervalDetail.getFirstBillingCycleDate(), lastBillingCycleDate, billingPeriod); + + for (int i = 0; i < numberOfWholeBillingPeriods; i++) { + final LocalDate servicePeriodStartDate; + if (results.size() > 0) { + // Make sure the periods align, especially with the pro-ration calculations above + servicePeriodStartDate = results.get(results.size() - 1).getEndDate(); + } else if (i == 0) { + // Use the specified start date + servicePeriodStartDate = startDate; + } else { + throw new IllegalStateException("We should at least have one invoice item!"); + } + + // Make sure to align the end date with the BCD + final LocalDate servicePeriodEndDate = billingIntervalDetail.getFutureBillingDateFor(i + 1); + results.add(new RecurringInvoiceItemData(servicePeriodStartDate, servicePeriodEndDate, BigDecimal.ONE)); + } + + // + // Now we check if indeed we need a trailing proration and add that incomplete item + // + if (effectiveEndDate.isAfter(lastBillingCycleDate)) { + final BigDecimal trailingProRationPeriods = calculateProRationAfterLastBillingCycleDate(effectiveEndDate, lastBillingCycleDate, billingPeriod); + if (trailingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) { + // Not common - add info in the logs for debugging purposes + final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(lastBillingCycleDate, effectiveEndDate, trailingProRationPeriods); + log.info("Adding trailing pro-ration: {}", itemData); + results.add(itemData); + } + } + return new RecurringInvoiceItemDataWithNextBillingCycleDate(results, billingIntervalDetail); + } + + private 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/InvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java index 8023ee9f29..e513a00007 100644 --- a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java +++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java @@ -17,13 +17,12 @@ package org.killbill.billing.invoice.generator; import java.util.List; -import java.util.UUID; import javax.annotation.Nullable; import org.joda.time.LocalDate; -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.Currency; import org.killbill.billing.invoice.api.Invoice; @@ -31,7 +30,6 @@ import org.killbill.billing.junction.BillingEventSet; public interface InvoiceGenerator { - - public Invoice generateInvoice(Account account, @Nullable BillingEventSet events, @Nullable List existingInvoices, - LocalDate targetDate, Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException; + InvoiceWithMetadata generateInvoice(ImmutableAccountData account, @Nullable BillingEventSet events, @Nullable List existingInvoices, + LocalDate targetDate, Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException; } diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java new file mode 100644 index 0000000000..d9b309098b --- /dev/null +++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java @@ -0,0 +1,43 @@ +/* + * 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.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.annotation.Nullable; + +import org.joda.time.LocalDate; +import org.killbill.billing.account.api.ImmutableAccountData; +import org.killbill.billing.callcontext.InternalCallContext; +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.junction.BillingEventSet; + +public abstract class InvoiceItemGenerator { + + public abstract 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 context) throws InvoiceApiException; + +} diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java new file mode 100644 index 0000000000..3673a4c283 --- /dev/null +++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java @@ -0,0 +1,197 @@ +/* + * 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.HashMap; +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.catalog.api.BillingMode; +import org.killbill.billing.invoice.api.Invoice; +import org.killbill.billing.invoice.api.InvoiceItem; +import org.killbill.billing.invoice.api.InvoiceItemType; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; + +public class InvoiceWithMetadata { + + private final Map perSubscriptionFutureNotificationDates; + + private Invoice invoice; + + public InvoiceWithMetadata(final Invoice originalInvoice, final Map perSubscriptionFutureNotificationDates) { + this.invoice = originalInvoice; + this.perSubscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates; + build(); + remove$0RecurringAndUsageItems(); + } + + public Invoice getInvoice() { + return invoice; + } + + public Map getPerSubscriptionFutureNotificationDates() { + return perSubscriptionFutureNotificationDates; + } + + // Remove all the IN_ADVANCE items for which we have no invoice items + private void build() { + // nextRecurringDate are computed based on *proposed* items, and not missing items (= proposed - existing). So + // we need to filter out the dates for which there is no item left otherwsie we may end up in creating too many notification dates + // and in particular that could lead to an infinite loop. + for (final UUID subscriptionId : perSubscriptionFutureNotificationDates.keySet()) { + final SubscriptionFutureNotificationDates tmp = perSubscriptionFutureNotificationDates.get(subscriptionId); + if (tmp.getRecurringBillingMode() == BillingMode.IN_ADVANCE && !hasItemsForSubscription(subscriptionId, InvoiceItemType.RECURRING)) { + tmp.resetNextRecurringDate(); + } + } + } + + private boolean hasItemsForSubscription(final UUID subscriptionId, final InvoiceItemType invoiceItemType) { + return invoice != null && Iterables.any(invoice.getInvoiceItems(), new Predicate() { + @Override + public boolean apply(final InvoiceItem input) { + return input.getInvoiceItemType() == invoiceItemType && + input.getSubscriptionId().equals(subscriptionId); + } + }); + } + + protected void remove$0RecurringAndUsageItems() { + if (invoice != null) { + final Iterator it = invoice.getInvoiceItems().iterator(); + while (it.hasNext()) { + final InvoiceItem item = it.next(); + if ((item.getInvoiceItemType() == InvoiceItemType.RECURRING || item.getInvoiceItemType() == InvoiceItemType.USAGE) && + item.getAmount().compareTo(BigDecimal.ZERO) == 0) { + it.remove(); + } + } + if (invoice.getInvoiceItems().isEmpty()) { + invoice = null; + } + } + } + + + public static class SubscriptionFutureNotificationDates { + + private final BillingMode recurringBillingMode; + + private LocalDate nextRecurringDate; + private Map nextUsageDates; + + public SubscriptionFutureNotificationDates(final BillingMode recurringBillingMode) { + this.recurringBillingMode = recurringBillingMode; + this.nextRecurringDate = null; + this.nextUsageDates = null; + } + + public void updateNextRecurringDateIfRequired(final LocalDate nextRecurringDateCandidate) { + if (nextRecurringDateCandidate != null) { + nextRecurringDate = getMaxDate(nextRecurringDate, nextRecurringDateCandidate); + } + } + + public void updateNextUsageDateIfRequired(final String usageName, final BillingMode billingMode, final LocalDate nextUsageDateCandidate) { + if (nextUsageDateCandidate != null) { + if (nextUsageDates == null) { + nextUsageDates = new HashMap(); + } + final UsageDef usageDef = new UsageDef(usageName, billingMode); + final LocalDate nextUsageDate = getMaxDate(nextUsageDates.get(usageDef), nextUsageDateCandidate); + nextUsageDates.put(usageDef, nextUsageDate); + } + } + + public LocalDate getNextRecurringDate() { + return nextRecurringDate; + } + + public Map getNextUsageDates() { + return nextUsageDates; + } + + public BillingMode getRecurringBillingMode() { + return recurringBillingMode; + } + + public void resetNextRecurringDate() { + nextRecurringDate = null; + } + + private static LocalDate getMaxDate(@Nullable final LocalDate existingDate, final LocalDate nextDateCandidate) { + if (existingDate == null) { + return nextDateCandidate; + } else { + return nextDateCandidate.compareTo(existingDate) > 0 ? nextDateCandidate : existingDate; + } + } + + public static class UsageDef { + + private final String usageName; + private final BillingMode billingMode; + + public UsageDef(final String usageName, final BillingMode billingMode) { + this.usageName = usageName; + this.billingMode = billingMode; + } + + public String getUsageName() { + return usageName; + } + + public BillingMode getBillingMode() { + return billingMode; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof UsageDef)) { + return false; + } + + final UsageDef usageDef = (UsageDef) o; + + if (usageName != null ? !usageName.equals(usageDef.usageName) : usageDef.usageName != null) { + return false; + } + return billingMode == usageDef.billingMode; + + } + + @Override + public int hashCode() { + int result = usageName != null ? usageName.hashCode() : 0; + result = 31 * result + (billingMode != null ? billingMode.hashCode() : 0); + return result; + } + } + } +} diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java new file mode 100644 index 0000000000..b19e77415b --- /dev/null +++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java @@ -0,0 +1,211 @@ +/* + * 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.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.annotation.Nullable; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.LocalDate; +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.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.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates; +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.invoice.usage.SubscriptionConsumableInArrear.SubscriptionConsumableInArrearItemsAndNextNotificationDate; +import org.killbill.billing.junction.BillingEvent; +import org.killbill.billing.junction.BillingEventSet; +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 UsageInvoiceItemGenerator extends InvoiceItemGenerator { + + private static final Logger log = LoggerFactory.getLogger(UsageInvoiceItemGenerator.class); + + private final RawUsageOptimizer rawUsageOptimizer; + + @Inject + public UsageInvoiceItemGenerator(final RawUsageOptimizer rawUsageOptimizer) { + this.rawUsageOptimizer = rawUsageOptimizer; + } + + + @Override + public List generateItems(final ImmutableAccountData account, + final UUID invoiceId, + final BillingEventSet eventSet, + @Nullable final List existingInvoices, + final LocalDate targetDate, + final Currency targetCurrency, + final Map perSubscriptionFutureNotificationDates, + final InternalCallContext internalCallContext) throws InvoiceApiException { + + final Map> perSubscriptionConsumableInArrearUsageItems = extractPerSubscriptionExistingConsumableInArrearUsageItems(eventSet.getUsages(), existingInvoices); + try { + + final LocalDate minBillingEventDate = getMinBillingEventDate(eventSet, account.getTimeZone()); + + 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(), account.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(minBillingEventDate, 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(account.getId(), invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate()); + final List consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId); + + final SubscriptionConsumableInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.of()); + final List newInArrearUsageItems = subscriptionResult.getInvoiceItems(); + items.addAll(newInArrearUsageItems); + updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, subscriptionResult.getPerUsageNotificationDates(), BillingMode.IN_ARREAR, perSubscriptionFutureNotificationDates); + curEvents = Lists.newArrayList(); + } + curSubscriptionId = subscriptionId; + curEvents.add(event); + } + if (curSubscriptionId != null) { + final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(account.getId(), invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate()); + final List consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId); + + final SubscriptionConsumableInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.of()); + final List newInArrearUsageItems = subscriptionResult.getInvoiceItems(); + items.addAll(newInArrearUsageItems); + updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, subscriptionResult.getPerUsageNotificationDates(), BillingMode.IN_ARREAR, perSubscriptionFutureNotificationDates); + } + return items; + + } catch (CatalogApiException e) { + throw new InvoiceApiException(e); + } + } + + + private LocalDate getMinBillingEventDate(final BillingEventSet eventSet, final DateTimeZone accountTimeZone) { + DateTime minDate = null; + final Iterator events = eventSet.iterator(); + while (events.hasNext()) { + final BillingEvent cur = events.next(); + if (minDate == null || minDate.compareTo(cur.getEffectiveDate()) > 0) { + minDate = cur.getEffectiveDate(); + } + } + return new LocalDate(minDate, accountTimeZone); + } + + private void updatePerSubscriptionNextNotificationUsageDate(final UUID subscriptionId, final Map nextBillingCycleDates, final BillingMode usageBillingMode, final Map perSubscriptionFutureNotificationDates) { + if (usageBillingMode == BillingMode.IN_ADVANCE) { + throw new IllegalStateException("Not implemented Yet)"); + } + + SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates.get(subscriptionId); + if (subscriptionFutureNotificationDates == null) { + subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates(null); + perSubscriptionFutureNotificationDates.put(subscriptionId, subscriptionFutureNotificationDates); + } + for (final String usageName : nextBillingCycleDates.keySet()) { + subscriptionFutureNotificationDates.updateNextUsageDateIfRequired(usageName, usageBillingMode, nextBillingCycleDates.get(usageName)); + } + } + + 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; + } +} diff --git a/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java b/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java index f106da5d06..1973ff2488 100644 --- a/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java +++ b/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java @@ -39,7 +39,9 @@ import org.killbill.billing.invoice.dao.DefaultInvoiceDao; import org.killbill.billing.invoice.dao.InvoiceDao; import org.killbill.billing.invoice.generator.DefaultInvoiceGenerator; +import org.killbill.billing.invoice.generator.FixedAndRecurringInvoiceItemGenerator; import org.killbill.billing.invoice.generator.InvoiceGenerator; +import org.killbill.billing.invoice.generator.UsageInvoiceItemGenerator; import org.killbill.billing.invoice.notification.DefaultNextBillingDateNotifier; import org.killbill.billing.invoice.notification.DefaultNextBillingDatePoster; import org.killbill.billing.invoice.notification.EmailInvoiceNotifier; @@ -133,6 +135,8 @@ protected void installTagHandler() { protected void installInvoiceGenerator() { bind(InvoiceGenerator.class).to(DefaultInvoiceGenerator.class).asEagerSingleton(); + bind(FixedAndRecurringInvoiceItemGenerator.class).asEagerSingleton(); + bind(UsageInvoiceItemGenerator.class).asEagerSingleton(); } protected void installInvoicePluginApi() { @@ -157,7 +161,8 @@ protected void configure() { installInvoicePaymentApi(); installInvoiceMigrationApi(); installResourceBundleFactory(); - bind(RawUsageOptimizer.class).asEagerSingleton();; + bind(RawUsageOptimizer.class).asEagerSingleton(); + ; bind(InvoiceApiHelper.class).asEagerSingleton(); } } diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/BillingModeGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/model/BillingModeGenerator.java deleted file mode 100644 index 4116f3c1ed..0000000000 --- a/invoice/src/main/java/org/killbill/billing/invoice/model/BillingModeGenerator.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.model; - -import java.util.List; - -import javax.annotation.Nullable; - -import org.joda.time.DateTimeZone; -import org.joda.time.LocalDate; - -import org.killbill.billing.catalog.api.BillingPeriod; - -public interface BillingModeGenerator { - - List generateInvoiceItemData(LocalDate startDate, @Nullable LocalDate endDate, LocalDate targetDate, - int billingCycleDay, BillingPeriod billingPeriod) throws InvalidDateSequenceException; -} diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java index abb867357a..54363abb5a 100644 --- a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java +++ b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java @@ -41,21 +41,22 @@ public class DefaultInvoicePayment extends EntityBase implements InvoicePayment private final Currency processedCurrency; private final String paymentCookieId; private final UUID linkedInvoicePaymentId; + private final Boolean isSuccess; public DefaultInvoicePayment(final InvoicePaymentType type, final UUID paymentId, final UUID invoiceId, final DateTime paymentDate, - final BigDecimal amount, final Currency currency, final Currency processedCurrency) { - this(UUIDs.randomUUID(), null, type, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, null, null); + final BigDecimal amount, final Currency currency, final Currency processedCurrency, final Boolean isSuccess) { + this(UUIDs.randomUUID(), null, type, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, null, null, isSuccess); } public DefaultInvoicePayment(final UUID id, final InvoicePaymentType type, final UUID paymentId, final UUID invoiceId, final DateTime paymentDate, @Nullable final BigDecimal amount, @Nullable final Currency currency, @Nullable final Currency processedCurrency, @Nullable final String paymentCookieId, @Nullable final UUID linkedInvoicePaymentId) { - this(id, null, type, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, paymentCookieId, linkedInvoicePaymentId); + this(id, null, type, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, paymentCookieId, linkedInvoicePaymentId, true); } public DefaultInvoicePayment(final UUID id, @Nullable final DateTime createdDate, final InvoicePaymentType type, final UUID paymentId, final UUID invoiceId, final DateTime paymentDate, @Nullable final BigDecimal amount, @Nullable final Currency currency, @Nullable final Currency processedCurrency, @Nullable final String paymentCookieId, - @Nullable final UUID linkedInvoicePaymentId) { + @Nullable final UUID linkedInvoicePaymentId, final Boolean isSuccess) { super(id, createdDate, createdDate); this.type = type; this.paymentId = paymentId; @@ -66,6 +67,7 @@ public DefaultInvoicePayment(final UUID id, @Nullable final DateTime createdDate this.processedCurrency =processedCurrency; this.paymentCookieId = paymentCookieId; this.linkedInvoicePaymentId = linkedInvoicePaymentId; + this.isSuccess = isSuccess; } public DefaultInvoicePayment(final InvoicePaymentModelDao invoicePaymentModelDao) { @@ -73,7 +75,8 @@ public DefaultInvoicePayment(final InvoicePaymentModelDao invoicePaymentModelDao invoicePaymentModelDao.getPaymentId(), invoicePaymentModelDao.getInvoiceId(), invoicePaymentModelDao.getPaymentDate(), invoicePaymentModelDao.getAmount(), invoicePaymentModelDao.getCurrency(), invoicePaymentModelDao.getProcessedCurrency(), invoicePaymentModelDao.getPaymentCookieId(), - invoicePaymentModelDao.getLinkedInvoicePaymentId()); + invoicePaymentModelDao.getLinkedInvoicePaymentId(), + invoicePaymentModelDao.getSuccess()); } @Override @@ -121,4 +124,9 @@ public Currency getProcessedCurrency() { return processedCurrency; } + @Override + public Boolean isSuccess() { + return isSuccess; + } + } diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/InAdvanceBillingMode.java b/invoice/src/main/java/org/killbill/billing/invoice/model/InAdvanceBillingMode.java deleted file mode 100644 index 31663ef39d..0000000000 --- a/invoice/src/main/java/org/killbill/billing/invoice/model/InAdvanceBillingMode.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.model; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -import javax.annotation.Nullable; - -import org.joda.time.LocalDate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.killbill.billing.catalog.api.BillingPeriod; -import org.killbill.billing.invoice.generator.BillingIntervalDetail; - -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 InAdvanceBillingMode implements BillingModeGenerator { - - private static final Logger log = LoggerFactory.getLogger(InAdvanceBillingMode.class); - - @Override - public List generateInvoiceItemData(final LocalDate startDate, @Nullable final LocalDate endDate, - final LocalDate targetDate, - final int billingCycleDayLocal, final BillingPeriod billingPeriod) throws InvalidDateSequenceException { - if (endDate != null && endDate.isBefore(startDate)) { - throw new InvalidDateSequenceException(); - } - if (targetDate.isBefore(startDate)) { - throw new InvalidDateSequenceException(); - } - - final List results = new ArrayList(); - - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(startDate, endDate, targetDate, billingCycleDayLocal, billingPeriod); - - // We are not billing for less than a day (we could...) - if (endDate != null && endDate.equals(startDate)) { - return results; - } - // - // If there is an endDate and that endDate is before our first coming firstBillingCycleDate, all we have to do - // is to charge for that period - // - if (endDate != null && !endDate.isAfter(billingIntervalDetail.getFirstBillingCycleDate())) { - final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, endDate, billingPeriod); - final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, endDate, leadingProRationPeriods); - results.add(itemData); - return results; - } - - // - // Leading proration if - // i) The first firstBillingCycleDate is strictly after our start date AND - // ii) The endDate is is not null and is strictly after our firstBillingCycleDate (previous check) - // - if (billingIntervalDetail.getFirstBillingCycleDate().isAfter(startDate)) { - final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, billingIntervalDetail.getFirstBillingCycleDate(), billingPeriod); - if (leadingProRationPeriods != null && leadingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) { - // Not common - add info in the logs for debugging purposes - final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, billingIntervalDetail.getFirstBillingCycleDate(), leadingProRationPeriods); - log.info("Adding pro-ration: {}", itemData); - results.add(itemData); - } - } - - // - // Calculate the effectiveEndDate from the firstBillingCycleDate: - // - If endDate != null and targetDate is after endDate => this is the endDate and will lead to a trailing pro-ration - // - If not, this is the last billingCycleDate calculation right after the targetDate - // - final LocalDate effectiveEndDate = billingIntervalDetail.getEffectiveEndDate(); - - // - // Based on what we calculated previously, code recompute one more time the numberOfWholeBillingPeriods - // - final LocalDate lastBillingCycleDate = billingIntervalDetail.getLastBillingCycleDate(); - final int numberOfWholeBillingPeriods = calculateNumberOfWholeBillingPeriods(billingIntervalDetail.getFirstBillingCycleDate(), lastBillingCycleDate, billingPeriod); - - for (int i = 0; i < numberOfWholeBillingPeriods; i++) { - final LocalDate servicePeriodStartDate; - if (results.size() > 0) { - // Make sure the periods align, especially with the pro-ration calculations above - servicePeriodStartDate = results.get(results.size() - 1).getEndDate(); - } else if (i == 0) { - // Use the specified start date - servicePeriodStartDate = startDate; - } else { - throw new IllegalStateException("We should at least have one invoice item!"); - } - - // Make sure to align the end date with the BCD - final LocalDate servicePeriodEndDate = billingIntervalDetail.getFutureBillingDateFor(i + 1); - results.add(new RecurringInvoiceItemData(servicePeriodStartDate, servicePeriodEndDate, BigDecimal.ONE)); - } - - // - // Now we check if indeed we need a trailing proration and add that incomplete item - // - if (effectiveEndDate.isAfter(lastBillingCycleDate)) { - final BigDecimal trailingProRationPeriods = calculateProRationAfterLastBillingCycleDate(effectiveEndDate, lastBillingCycleDate, billingPeriod); - if (trailingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) { - // Not common - add info in the logs for debugging purposes - final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(lastBillingCycleDate, effectiveEndDate, trailingProRationPeriods); - log.info("Adding trailing pro-ration: {}", itemData); - results.add(itemData); - } - } - return results; - } -} diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/RecurringInvoiceItemDataWithNextBillingCycleDate.java b/invoice/src/main/java/org/killbill/billing/invoice/model/RecurringInvoiceItemDataWithNextBillingCycleDate.java new file mode 100644 index 0000000000..97e1b67562 --- /dev/null +++ b/invoice/src/main/java/org/killbill/billing/invoice/model/RecurringInvoiceItemDataWithNextBillingCycleDate.java @@ -0,0 +1,43 @@ +/* + * 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.model; + +import java.util.List; + +import org.joda.time.LocalDate; +import org.killbill.billing.catalog.api.BillingMode; +import org.killbill.billing.invoice.generator.BillingIntervalDetail; + +public class RecurringInvoiceItemDataWithNextBillingCycleDate { + + private final List itemData; + private final BillingIntervalDetail billingIntervalDetail; + + public RecurringInvoiceItemDataWithNextBillingCycleDate(final List itemData, final BillingIntervalDetail billingIntervalDetail) { + this.itemData = itemData; + this.billingIntervalDetail = billingIntervalDetail; + } + + public List getItemData() { + return itemData; + } + + public LocalDate getNextBillingCycleDate() { + return billingIntervalDetail.getNextBillingCycleDate(); + } +} diff --git a/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java b/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java index 826782c467..6b2883d7c7 100644 --- a/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java +++ b/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java @@ -38,7 +38,7 @@ public DefaultNoOpInvoiceProviderPlugin(final Clock clock) { } @Override - public List getAdditionalInvoiceItems(final Invoice invoice, Iterable properties, CallContext context) { + public List getAdditionalInvoiceItems(final Invoice invoice, final boolean isDryRun, final Iterable properties, CallContext context) { return ImmutableList.of(); } } diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java index 8ae278acfc..7ebb8d2a2a 100644 --- a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java +++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.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: * @@ -17,24 +19,22 @@ package org.killbill.billing.invoice.template.formatters; import java.math.BigDecimal; -import java.text.NumberFormat; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.UUID; +import org.joda.money.CurrencyUnit; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.killbill.billing.callcontext.InternalTenantContext; -import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory; -import org.killbill.billing.tenant.api.TenantInternalApi; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import org.killbill.billing.catalog.api.Currency; import org.killbill.billing.currency.api.CurrencyConversion; import org.killbill.billing.currency.api.CurrencyConversionApi; @@ -45,23 +45,24 @@ import org.killbill.billing.invoice.api.InvoiceItemType; import org.killbill.billing.invoice.api.InvoicePayment; import org.killbill.billing.invoice.api.formatters.InvoiceFormatter; +import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory; import org.killbill.billing.invoice.model.CreditAdjInvoiceItem; import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem; import org.killbill.billing.invoice.model.DefaultInvoice; import org.killbill.billing.util.template.translation.TranslatorConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import com.google.common.base.Objects; +import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; -import static org.killbill.billing.util.DefaultAmountFormatter.round; - /** * Format invoice fields */ public class DefaultInvoiceFormatter implements InvoiceFormatter { - private final static Logger logger = LoggerFactory.getLogger(DefaultInvoiceFormatter.class); + private static final Logger logger = LoggerFactory.getLogger(DefaultInvoiceFormatter.class); private final TranslatorConfig config; private final Invoice invoice; @@ -70,8 +71,11 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter { private final CurrencyConversionApi currencyConversionApi; private final InternalTenantContext context; private final ResourceBundleFactory bundleFactory; + private final Map currencyLocaleMap; - public DefaultInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, final CurrencyConversionApi currencyConversionApi, final ResourceBundleFactory bundleFactory, final InternalTenantContext context) { + public DefaultInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, + final CurrencyConversionApi currencyConversionApi, final ResourceBundleFactory bundleFactory, + final InternalTenantContext context, final Map currencyLocaleMap) { this.config = config; this.invoice = invoice; this.dateFormatter = DateTimeFormat.mediumDate().withLocale(locale); @@ -79,11 +83,12 @@ public DefaultInvoiceFormatter(final TranslatorConfig config, final Invoice invo this.currencyConversionApi = currencyConversionApi; this.bundleFactory = bundleFactory; this.context = context; + this.currencyLocaleMap = currencyLocaleMap; } @Override public Integer getInvoiceNumber() { - return Objects.firstNonNull(invoice.getInvoiceNumber(), 0); + return MoreObjects.firstNonNull(invoice.getInvoiceNumber(), 0); } @Override @@ -163,7 +168,7 @@ public boolean addInvoiceItems(final Collection items) { @Override public List getInvoiceItems(final Class clazz) { - return Objects.firstNonNull(invoice.getInvoiceItems(clazz), ImmutableList.of()); + return MoreObjects.firstNonNull(invoice.getInvoiceItems(clazz), ImmutableList.of()); } @Override @@ -183,7 +188,7 @@ public boolean addPayments(final Collection payments) { @Override public List getPayments() { - return Objects.firstNonNull(invoice.getPayments(), ImmutableList.of()); + return MoreObjects.firstNonNull(invoice.getPayments(), ImmutableList.of()); } @Override @@ -198,35 +203,55 @@ public UUID getAccountId() { @Override public BigDecimal getChargedAmount() { - return round(Objects.firstNonNull(invoice.getChargedAmount(), BigDecimal.ZERO)); + return MoreObjects.firstNonNull(invoice.getChargedAmount(), BigDecimal.ZERO); } @Override public BigDecimal getOriginalChargedAmount() { - return round(Objects.firstNonNull(invoice.getOriginalChargedAmount(), BigDecimal.ZERO)); + return MoreObjects.firstNonNull(invoice.getOriginalChargedAmount(), BigDecimal.ZERO); } @Override public BigDecimal getBalance() { - return round(Objects.firstNonNull(invoice.getBalance(), BigDecimal.ZERO)); + return MoreObjects.firstNonNull(invoice.getBalance(), BigDecimal.ZERO); } @Override public String getFormattedChargedAmount() { - final NumberFormat number = NumberFormat.getCurrencyInstance(locale); - return number.format(getChargedAmount().doubleValue()); + return getFormattedAmountByLocaleAndInvoiceCurrency(getChargedAmount()); } @Override public String getFormattedPaidAmount() { - final NumberFormat number = NumberFormat.getCurrencyInstance(locale); - return number.format(getPaidAmount().doubleValue()); + return getFormattedAmountByLocaleAndInvoiceCurrency(getPaidAmount()); } @Override public String getFormattedBalance() { - final NumberFormat number = NumberFormat.getCurrencyInstance(locale); - return number.format(getBalance().doubleValue()); + return getFormattedAmountByLocaleAndInvoiceCurrency(getBalance()); + } + + // Returns the formatted amount with the correct currency symbol that is get from the invoice currency. + private String getFormattedAmountByLocaleAndInvoiceCurrency(final BigDecimal amount) { + final String invoiceCurrencyCode = invoice.getCurrency().toString(); + final CurrencyUnit currencyUnit = CurrencyUnit.of(invoiceCurrencyCode); + + final DecimalFormat numberFormatter = (DecimalFormat) DecimalFormat.getCurrencyInstance(locale); + final DecimalFormatSymbols dfs = numberFormatter.getDecimalFormatSymbols(); + dfs.setInternationalCurrencySymbol(currencyUnit.getCurrencyCode()); + + try { + final java.util.Currency currency = java.util.Currency.getInstance(invoiceCurrencyCode); + dfs.setCurrencySymbol(currency.getSymbol(currencyLocaleMap.get(currency))); + } catch (final IllegalArgumentException e) { + dfs.setCurrencySymbol(currencyUnit.getSymbol(locale)); + } + + numberFormatter.setDecimalFormatSymbols(dfs); + numberFormatter.setMinimumFractionDigits(currencyUnit.getDefaultFractionDigits()); + numberFormatter.setMaximumFractionDigits(currencyUnit.getDefaultFractionDigits()); + + return numberFormatter.format(amount.doubleValue()); } @Override @@ -244,7 +269,7 @@ public String getProcessedPaymentRate() { } // If there were multiple payments (and refunds) we pick chose the last one DateTime latestPaymentDate = null; - final Iterator paymentIterator = ((DefaultInvoice) invoice).getPayments().iterator(); + final Iterator paymentIterator = invoice.getPayments().iterator(); while (paymentIterator.hasNext()) { final InvoicePayment cur = paymentIterator.next(); latestPaymentDate = latestPaymentDate != null && latestPaymentDate.isAfter(cur.getPaymentDate()) ? @@ -253,12 +278,12 @@ public String getProcessedPaymentRate() { } try { final CurrencyConversion conversion = currencyConversionApi.getCurrencyConversion(currency, latestPaymentDate); - for (Rate rate : conversion.getRates()) { + for (final Rate rate : conversion.getRates()) { if (rate.getCurrency() == getCurrency()) { return rate.getValue().toString(); } } - } catch (CurrencyConversionException e) { + } catch (final CurrencyConversionException e) { logger.warn("Failed to retrieve currency conversion rates for currency = " + currency + " and date = " + latestPaymentDate, e); return null; } @@ -288,7 +313,7 @@ public Currency getCurrency() { @Override public BigDecimal getPaidAmount() { - return round(Objects.firstNonNull(invoice.getPaidAmount(), BigDecimal.ZERO)); + return MoreObjects.firstNonNull(invoice.getPaidAmount(), BigDecimal.ZERO); } @Override @@ -339,11 +364,11 @@ protected Invoice getInvoice() { @Override public BigDecimal getCreditedAmount() { - return round(Objects.firstNonNull(invoice.getCreditedAmount(), BigDecimal.ZERO)); + return MoreObjects.firstNonNull(invoice.getCreditedAmount(), BigDecimal.ZERO); } @Override public BigDecimal getRefundedAmount() { - return round(Objects.firstNonNull(invoice.getRefundedAmount(), BigDecimal.ZERO)); + return MoreObjects.firstNonNull(invoice.getRefundedAmount(), BigDecimal.ZERO); } } diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java index 890fb30338..b9e405bb64 100644 --- a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java +++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java @@ -16,7 +16,10 @@ package org.killbill.billing.invoice.template.formatters; +import java.util.Currency; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import org.killbill.billing.callcontext.InternalTenantContext; import org.killbill.billing.currency.api.CurrencyConversionApi; @@ -24,14 +27,33 @@ import org.killbill.billing.invoice.api.formatters.InvoiceFormatter; import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory; import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory; -import org.killbill.billing.tenant.api.TenantInternalApi; import org.killbill.billing.util.template.translation.TranslatorConfig; +import com.google.common.annotations.VisibleForTesting; + public class DefaultInvoiceFormatterFactory implements InvoiceFormatterFactory { + private final Map currencyLocaleMap = new HashMap(); + + public DefaultInvoiceFormatterFactory() { + // This initialization relies on System.currentTimeMillis() instead of the Kill Bill clock (it won't be accurate when moving the clock) + for (final Locale localeItem : Locale.getAvailableLocales()) { + try { + final java.util.Currency currency = java.util.Currency.getInstance(localeItem); + currencyLocaleMap.put(currency, localeItem); + } catch (final Exception ignored) { + } + } + } + @Override - public InvoiceFormatter createInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, CurrencyConversionApi currencyConversionApi, + public InvoiceFormatter createInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, final CurrencyConversionApi currencyConversionApi, final ResourceBundleFactory bundleFactory, final InternalTenantContext context) { - return new DefaultInvoiceFormatter(config, invoice, locale, currencyConversionApi, bundleFactory, context); + return new DefaultInvoiceFormatter(config, invoice, locale, currencyConversionApi, bundleFactory, context, currencyLocaleMap); + } + + @VisibleForTesting + Map getCurrencyLocaleMap() { + return currencyLocaleMap; } } diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java index 51ddcc637a..5516c8ca90 100644 --- a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java +++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java @@ -36,14 +36,10 @@ import org.killbill.billing.util.template.translation.DefaultCatalogTranslator; import org.killbill.billing.util.template.translation.Translator; import org.killbill.billing.util.template.translation.TranslatorConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.google.common.base.Objects; import com.google.common.base.Strings; -import static org.killbill.billing.util.DefaultAmountFormatter.round; - /** * Format invoice item fields */ @@ -71,7 +67,7 @@ public DefaultInvoiceItemFormatter(final TranslatorConfig config, @Override public BigDecimal getAmount() { - return round(Objects.firstNonNull(item.getAmount(), BigDecimal.ZERO)); + return Objects.firstNonNull(item.getAmount(), BigDecimal.ZERO); } @Override @@ -168,7 +164,7 @@ public DateTime getUpdatedDate() { @Override public BigDecimal getRate() { - return round(BigDecimal.ZERO); + return BigDecimal.ZERO; } @Override diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableInArrear.java index f1747c2ab7..bdda28dd74 100644 --- a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableInArrear.java +++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableInArrear.java @@ -28,6 +28,7 @@ import org.joda.time.DateTimeZone; import org.joda.time.LocalDate; +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.TieredBlock; @@ -68,12 +69,14 @@ public class ContiguousIntervalConsumableInArrear { private final Set unitTypes; private final List rawSubscriptionUsage; private final LocalDate targetDate; + private final UUID accountId; private final UUID invoiceId; private final AtomicBoolean isBuilt; private final LocalDate rawUsageStartDate; - public ContiguousIntervalConsumableInArrear(final Usage usage, final UUID invoiceId, final List rawSubscriptionUsage, final LocalDate targetDate, final LocalDate rawUsageStartDate) { + public ContiguousIntervalConsumableInArrear(final Usage usage, final UUID accountId, final UUID invoiceId, final List rawSubscriptionUsage, final LocalDate targetDate, final LocalDate rawUsageStartDate) { this.usage = usage; + this.accountId = accountId; this.invoiceId = invoiceId; this.unitTypes = getConsumableInArrearUnitTypes(usage); this.rawSubscriptionUsage = rawSubscriptionUsage; @@ -106,7 +109,7 @@ public ContiguousIntervalConsumableInArrear build(final boolean closedInterval) } final LocalDate endDate = closedInterval ? new LocalDate(billingEvents.get(billingEvents.size() - 1).getEffectiveDate(), getAccountTimeZone()) : targetDate; - final BillingIntervalDetail bid = new BillingIntervalDetail(startDate, endDate, targetDate, getBCD(), usage.getBillingPeriod()); + final BillingIntervalDetail bid = new BillingIntervalDetail(startDate, endDate, targetDate, getBCD(), usage.getBillingPeriod(), usage.getBillingMode()); int numberOfPeriod = 0; // First billingCycleDate prior startDate @@ -123,10 +126,32 @@ public ContiguousIntervalConsumableInArrear build(final boolean closedInterval) numberOfPeriod++; nextBillCycleDate = bid.getFutureBillingDateFor(numberOfPeriod); } + if (closedInterval && endDate.isAfter(transitionTimes.get(transitionTimes.size() - 1))) { + transitionTimes.add(endDate); + } isBuilt.set(true); return this; } + public class ConsumableInArrearItemsAndNextNotificationDate { + private final List invoiceItems; + private final LocalDate nextNotificationDate; + + public ConsumableInArrearItemsAndNextNotificationDate(final List invoiceItems, final LocalDate nextNotificationDate) { + this.invoiceItems = invoiceItems; + this.nextNotificationDate = nextNotificationDate; + } + + public List getInvoiceItems() { + return invoiceItems; + } + + public LocalDate getNextNotificationDate() { + return nextNotificationDate; + } + } + + /** * Compute the missing usage invoice items based on what should be billed and what has been billed ($ amount comparison). * @@ -134,12 +159,12 @@ public ContiguousIntervalConsumableInArrear build(final boolean closedInterval) * @return * @throws CatalogApiException */ - public List computeMissingItems(final List existingUsage) throws CatalogApiException { + public ConsumableInArrearItemsAndNextNotificationDate computeMissingItemsAndNextNotificationDate(final List existingUsage) throws CatalogApiException { Preconditions.checkState(isBuilt.get()); if (transitionTimes.size() < 2) { - return ImmutableList.of(); + return new ConsumableInArrearItemsAndNextNotificationDate(ImmutableList.of(), null); } final List result = Lists.newLinkedList(); @@ -150,7 +175,7 @@ public List computeMissingItems(final List existingUsa LocalDate prevDate = null; for (LocalDate curDate : transitionTimes) { if (prevDate != null) { - InvoiceItem item = new UsageInvoiceItem(invoiceId, getAccountId(), getBundleId(), getSubscriptionId(), getPlanName(), + InvoiceItem item = new UsageInvoiceItem(invoiceId, accountId, getBundleId(), getSubscriptionId(), getPlanName(), getPhaseName(), usage.getName(), prevDate, curDate, BigDecimal.ZERO, getCurrency()); result.add(item); } @@ -178,15 +203,40 @@ public List computeMissingItems(final List existingUsa if (!billedItems.iterator().hasNext() || billedUsage.compareTo(toBeBilledUsage) < 0) { final BigDecimal amountToBill = toBeBilledUsage.subtract(billedUsage); if (amountToBill.compareTo(BigDecimal.ZERO) > 0) { - InvoiceItem item = new UsageInvoiceItem(invoiceId, getAccountId(), getBundleId(), getSubscriptionId(), getPlanName(), + InvoiceItem item = new UsageInvoiceItem(invoiceId, accountId, getBundleId(), getSubscriptionId(), getPlanName(), getPhaseName(), usage.getName(), ru.getStart(), ru.getEnd(), amountToBill, getCurrency()); result.add(item); } } } + + final LocalDate nextNotificationdate = computeNextNotificationDate(); + return new ConsumableInArrearItemsAndNextNotificationDate(result, nextNotificationdate); + } + + private LocalDate computeNextNotificationDate() { + LocalDate result = null; + final Iterator eventIt = billingEvents.iterator(); + BillingEvent nextEvent = eventIt.next(); + while (eventIt.hasNext()) { + final BillingEvent thisEvent = nextEvent; + nextEvent = eventIt.next(); + final LocalDate startDate = new LocalDate(thisEvent.getEffectiveDate(), thisEvent.getTimeZone()); + final LocalDate endDate = new LocalDate(nextEvent.getEffectiveDate(), nextEvent.getTimeZone()); + + final BillingIntervalDetail bid = new BillingIntervalDetail(startDate, endDate, targetDate, thisEvent.getBillCycleDayLocal(), usage.getBillingPeriod(), BillingMode.IN_ARREAR); + final LocalDate nextBillingCycleDate = bid.getNextBillingCycleDate(); + result = (result == null || result.compareTo(nextBillingCycleDate) < 0) ? nextBillingCycleDate : result; + } + + final LocalDate startDate = new LocalDate(nextEvent.getEffectiveDate(), nextEvent.getTimeZone()); + final BillingIntervalDetail bid = new BillingIntervalDetail(startDate, null, targetDate, nextEvent.getBillCycleDayLocal(), usage.getBillingPeriod(), BillingMode.IN_ARREAR); + final LocalDate nextBillingCycleDate = bid.getNextBillingCycleDate(); + result = (result == null || result.compareTo(nextBillingCycleDate) < 0) ? nextBillingCycleDate : result; return result; } + @VisibleForTesting List getRolledUpUsage() { @@ -355,9 +405,6 @@ public int getBCD() { return billingEvents.get(0).getBillCycleDayLocal(); } - public UUID getAccountId() { - return billingEvents.get(0).getAccount().getId(); - } public UUID getBundleId() { return billingEvents.get(0).getSubscription().getBundleId(); diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionConsumableInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionConsumableInArrear.java index 66f7449894..9a531a7cf6 100644 --- a/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionConsumableInArrear.java +++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionConsumableInArrear.java @@ -20,6 +20,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -31,6 +32,7 @@ import org.killbill.billing.catalog.api.Usage; import org.killbill.billing.catalog.api.UsageType; import org.killbill.billing.invoice.api.InvoiceItem; +import org.killbill.billing.invoice.usage.ContiguousIntervalConsumableInArrear.ConsumableInArrearItemsAndNextNotificationDate; import org.killbill.billing.junction.BillingEvent; import org.killbill.billing.usage.RawUsage; @@ -38,6 +40,8 @@ import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; +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.common.collect.Ordering; @@ -64,13 +68,15 @@ public int compare(final RawUsage o1, final RawUsage o2) { } }; + private final UUID accountId; private final UUID invoiceId; private final List subscriptionBillingEvents; private final LocalDate targetDate; private final List rawSubscriptionUsage; private final LocalDate rawUsageStartDate; - public SubscriptionConsumableInArrear(final UUID invoiceId, final List subscriptionBillingEvents, final List rawUsage, final LocalDate targetDate, final LocalDate rawUsageStartDate) { + public SubscriptionConsumableInArrear(final UUID accountId, final UUID invoiceId, final List subscriptionBillingEvents, final List rawUsage, final LocalDate targetDate, final LocalDate rawUsageStartDate) { + this.accountId = accountId; this.invoiceId = invoiceId; this.subscriptionBillingEvents = subscriptionBillingEvents; this.targetDate = targetDate; @@ -84,6 +90,7 @@ public boolean apply(final RawUsage input) { })); } + /** * Based on billing events, (@code existingUsage} and targetDate, figure out what remains to be billed. * @@ -91,16 +98,18 @@ public boolean apply(final RawUsage input) { * @return * @throws CatalogApiException */ - public List computeMissingUsageInvoiceItems(final List existingUsage) throws CatalogApiException { + public SubscriptionConsumableInArrearItemsAndNextNotificationDate computeMissingUsageInvoiceItems(final List existingUsage) throws CatalogApiException { - final List result = Lists.newLinkedList(); + final SubscriptionConsumableInArrearItemsAndNextNotificationDate result = new SubscriptionConsumableInArrearItemsAndNextNotificationDate(); final List billingEventTransitionTimePeriods = computeInArrearUsageInterval(); for (ContiguousIntervalConsumableInArrear usageInterval : billingEventTransitionTimePeriods) { - result.addAll(usageInterval.computeMissingItems(existingUsage)); + result.addConsumableInArrearItemsAndNextNotificationDate(usageInterval.getUsage().getName(), usageInterval.computeMissingItemsAndNextNotificationDate(existingUsage)); } return result; } + + @VisibleForTesting List computeInArrearUsageInterval() { @@ -129,7 +138,7 @@ public String apply(final Usage input) { // Add inflight usage interval if non existent ContiguousIntervalConsumableInArrear existingInterval = inFlightInArrearUsageIntervals.get(usage.getName()); if (existingInterval == null) { - existingInterval = new ContiguousIntervalConsumableInArrear(usage, invoiceId, rawSubscriptionUsage, targetDate, rawUsageStartDate); + existingInterval = new ContiguousIntervalConsumableInArrear(usage, accountId, invoiceId, rawSubscriptionUsage, targetDate, rawUsageStartDate); inFlightInArrearUsageIntervals.put(usage.getName(), existingInterval); } // Add billing event for that usage interval @@ -169,4 +178,40 @@ List findConsumableInArrearUsages(final BillingEvent event) { } return result; } + + public class SubscriptionConsumableInArrearItemsAndNextNotificationDate { + private List invoiceItems; + private Map perUsageNotificationDates; + + public SubscriptionConsumableInArrearItemsAndNextNotificationDate() { + this.invoiceItems = null; + this.perUsageNotificationDates = null; + } + + public void addConsumableInArrearItemsAndNextNotificationDate(final String usageName, final ConsumableInArrearItemsAndNextNotificationDate input) { + if (!input.getInvoiceItems().isEmpty()) { + if (invoiceItems == null) { + invoiceItems = new LinkedList(); + } + invoiceItems.addAll(input.getInvoiceItems()); + + } + + if (input.getNextNotificationDate() != null) { + if (perUsageNotificationDates == null) { + perUsageNotificationDates = new HashMap(); + } + perUsageNotificationDates.put(usageName, input.getNextNotificationDate()); + } + } + + public List getInvoiceItems() { + return invoiceItems != null ? invoiceItems : ImmutableList.of(); + } + + public Map getPerUsageNotificationDates() { + return perUsageNotificationDates != null ? perUsageNotificationDates : ImmutableMap.of(); + } + } + } diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg index fe0410fac9..b9e4dab87b 100644 --- a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg +++ b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg @@ -12,6 +12,7 @@ tableFields(prefix) ::= << , processed_currency , payment_cookie_id , linked_invoice_payment_id +, success , created_by , created_date >> @@ -26,6 +27,7 @@ tableValues() ::= << , :processedCurrency , :paymentCookieId , :linkedInvoicePaymentId +, :success , :createdBy , :createdDate >> @@ -70,6 +72,7 @@ getRemainingAmountPaid() ::= << SELECT SUM(amount) FROM WHERE (id = :invoicePaymentId OR linked_invoice_payment_id = :invoicePaymentId) + AND success ; >> @@ -103,3 +106,14 @@ getChargebacksByPaymentId() ::= << ; >> + +updateAttempt() ::= << + UPDATE + SET success = true, + payment_date = :paymentDate, + amount = :amount, + processed_currency = :processedCurrency + WHERE record_id = :recordId + + ; +>> diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql b/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql index f8b1375d46..bb1ed4ed81 100644 --- a/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql +++ b/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql @@ -63,6 +63,7 @@ CREATE TABLE invoice_payments ( processed_currency varchar(3) NOT NULL, payment_cookie_id varchar(255) DEFAULT NULL, linked_invoice_payment_id varchar(36) DEFAULT NULL, + success bool DEFAULT true, created_by varchar(50) NOT NULL, created_date datetime NOT NULL, account_record_id bigint /*! unsigned */ not null, diff --git a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java index 1069bfc656..2e2692e95a 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java @@ -27,6 +27,7 @@ import org.killbill.billing.invoice.api.InvoiceUserApi; import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory; import org.killbill.billing.invoice.dao.InvoiceDao; +import org.killbill.billing.invoice.generator.FixedAndRecurringInvoiceItemGenerator; import org.killbill.billing.invoice.generator.InvoiceGenerator; import org.killbill.billing.invoice.glue.TestInvoiceModuleNoDB; import org.killbill.billing.invoice.usage.RawUsageOptimizer; @@ -97,7 +98,8 @@ public abstract class InvoiceTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB { protected ResourceBundleFactory resourceBundleFactory; @Inject protected RawUsageOptimizer rawUsageOptimizer; - + @Inject + protected FixedAndRecurringInvoiceItemGenerator fixedAndRecurringInvoiceItemGenerator; @Override protected KillbillConfigSource getConfigSource() { return getConfigSource("/resource.properties"); diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java index 5929b0b9ff..2bdde603db 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java @@ -19,12 +19,10 @@ package org.killbill.billing.invoice; import java.math.BigDecimal; -import java.util.Collections; import java.util.List; 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.AccountApiException; @@ -38,8 +36,6 @@ import org.killbill.billing.catalog.api.PhaseType; import org.killbill.billing.catalog.api.Plan; import org.killbill.billing.catalog.api.PlanPhase; -import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications; -import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification; import org.killbill.billing.invoice.TestInvoiceHelper.DryRunFutureDateArguments; import org.killbill.billing.invoice.api.DryRunArguments; import org.killbill.billing.invoice.api.Invoice; @@ -47,14 +43,11 @@ import org.killbill.billing.invoice.api.InvoiceItem; import org.killbill.billing.invoice.api.InvoiceItemType; import org.killbill.billing.invoice.api.InvoiceNotifier; -import org.killbill.billing.invoice.dao.InvoiceItemModelDao; import org.killbill.billing.invoice.dao.InvoiceModelDao; import org.killbill.billing.invoice.notification.NullInvoiceNotifier; import org.killbill.billing.junction.BillingEventSet; import org.killbill.billing.subscription.api.SubscriptionBase; import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; -import org.killbill.billing.util.timezone.DateAndTimeZoneContext; -import org.killbill.clock.ClockMock; import org.mockito.Mockito; import org.testng.Assert; import org.testng.annotations.BeforeMethod; @@ -82,7 +75,7 @@ public void testDryRunInvoice() throws InvoiceApiException, AccountApiException, final BillingEventSet events = new MockBillingEventSet(); final Plan plan = MockPlan.createBicycleNoTrialEvergreen1USD(); final PlanPhase planPhase = MockPlanPhase.create1USDMonthlyEvergreen(); - final DateTime effectiveDate = new DateTime().minusDays(1); + final DateTime effectiveDate = clock.getUTCNow().minusDays(1); final Currency currency = Currency.USD; final BigDecimal fixedPrice = null; events.add(invoiceUtil.createMockBillingEvent(account, subscription, effectiveDate, plan, planPhase, @@ -91,7 +84,7 @@ public void testDryRunInvoice() throws InvoiceApiException, AccountApiException, Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(events); - final DateTime target = new DateTime(); + final DateTime target = effectiveDate; final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier(); final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao, @@ -190,35 +183,4 @@ public void testWithOverdueEvents() throws Exception { Assert.assertEquals(item.getSubscriptionId(), subscription.getId()); } } - - @Test(groups = "slow") - public void testCreateNextFutureNotificationDate() throws Exception { - - final LocalDate startDate = new LocalDate("2012-10-26"); - final LocalDate endDate = new LocalDate("2012-11-26"); - - ((ClockMock) clock).setTime(new DateTime(2012, 10, 13, 1, 12, 23, DateTimeZone.UTC)); - - final DateAndTimeZoneContext dateAndTimeZoneContext = new DateAndTimeZoneContext(clock.getUTCNow(), DateTimeZone.forID("Pacific/Pitcairn"), clock); - - final InvoiceItemModelDao item = new InvoiceItemModelDao(UUID.randomUUID(), clock.getUTCNow(), InvoiceItemType.RECURRING, UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), - null, "planName", "phaseName", null, startDate, endDate, new BigDecimal("23.9"), new BigDecimal("23.9"), Currency.EUR, null); - - final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier(); - final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao, - internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(), - null, invoiceConfig, clock); - - final FutureAccountNotifications futureAccountNotifications = dispatcher.createNextFutureNotificationDate(Collections.singletonList(item), null, dateAndTimeZoneContext, context); - - Assert.assertEquals(futureAccountNotifications.getNotifications().size(), 1); - - final List receivedDates = futureAccountNotifications.getNotifications().get(item.getSubscriptionId()); - Assert.assertEquals(receivedDates.size(), 1); - - final LocalDate receivedTargetDate = new LocalDate(receivedDates.get(0).getEffectiveDate(), DateTimeZone.forID("Pacific/Pitcairn")); - Assert.assertEquals(receivedTargetDate, endDate); - - Assert.assertTrue(receivedDates.get(0).getEffectiveDate().compareTo(new DateTime(2012, 11, 27, 1, 12, 23, DateTimeZone.UTC)) <= 0); - } } diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java index 9d8dc34633..0ff2d289b7 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java @@ -52,6 +52,7 @@ import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications; import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification; 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; @@ -331,11 +332,6 @@ public BillingEvent createMockBillingEvent(@Nullable final Account account, fina Mockito.when(mockAccount.getTimeZone()).thenReturn(DateTimeZone.UTC); final Account accountOrMockAcount = account != null ? account : mockAccount; return new BillingEvent() { - @Override - public Account getAccount() { - return accountOrMockAcount; - } - @Override public int getBillCycleDayLocal() { return billCycleDayLocal; @@ -366,11 +362,6 @@ public BillingPeriod getBillingPeriod() { return billingPeriod; } - @Override - public BillingMode getBillingMode() { - return billingMode; - } - @Override public String getDescription() { return description; @@ -431,6 +422,11 @@ public static class DryRunFutureDateArguments implements DryRunArguments { public DryRunFutureDateArguments() { } + @Override + public DryRunType getDryRunType() { + return DryRunType.TARGET_DATE; + } + @Override public PlanPhaseSpecifier getPlanPhaseSpecifier() { return null; @@ -462,7 +458,7 @@ public BillingActionPolicy getBillingActionPolicy() { } @Override - public List getPlanPhasePriceoverrides() { + public List getPlanPhasePriceOverrides() { return null; } } diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java b/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java index 5f9477d5f2..0af10180aa 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java @@ -33,8 +33,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import static org.killbill.billing.invoice.tests.InvoiceTestUtils.createAndPersistInvoice; -import static org.killbill.billing.invoice.tests.InvoiceTestUtils.createAndPersistPayment; +import static org.killbill.billing.invoice.proRations.InvoiceTestUtils.createAndPersistInvoice; +import static org.killbill.billing.invoice.proRations.InvoiceTestUtils.createAndPersistPayment; public class TestDefaultInvoicePaymentApi extends InvoiceTestSuiteWithEmbeddedDB { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java index 97cd232dfd..74d938e2b2 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java @@ -32,6 +32,7 @@ import org.joda.time.LocalDate; import org.killbill.billing.ErrorCode; import org.killbill.billing.account.api.Account; +import org.killbill.billing.account.api.DefaultAccount; import org.killbill.billing.callcontext.InternalCallContext; import org.killbill.billing.catalog.DefaultPrice; import org.killbill.billing.catalog.MockInternationalPrice; @@ -53,6 +54,7 @@ import org.killbill.billing.invoice.api.InvoiceItemType; import org.killbill.billing.invoice.api.InvoicePayment; import org.killbill.billing.invoice.api.InvoicePaymentType; +import org.killbill.billing.invoice.generator.InvoiceWithMetadata; import org.killbill.billing.invoice.model.CreditAdjInvoiceItem; import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem; import org.killbill.billing.invoice.model.DefaultInvoice; @@ -149,7 +151,7 @@ public void testInvoicePayment() throws InvoiceApiException { final BigDecimal paymentAmount = new BigDecimal("11.00"); final UUID paymentId = UUID.randomUUID(); - final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, clock.getUTCNow().plusDays(12), paymentAmount, Currency.USD, Currency.USD); + final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, clock.getUTCNow().plusDays(12), paymentAmount, Currency.USD, Currency.USD, true); invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context); final InvoiceModelDao retrievedInvoice = invoiceDao.getById(invoiceId, context); @@ -520,7 +522,7 @@ public void testAccountBalance() throws EntityPersistenceException { invoiceUtil.createInvoiceItem(item2, context); final BigDecimal payment1 = new BigDecimal("48.0"); - final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD); + final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, true); invoiceUtil.createPayment(payment, context); final BigDecimal balance = invoiceDao.getAccountBalance(accountId, context); @@ -556,7 +558,7 @@ public void testAccountBalanceWithNoPayments() throws EntityPersistenceException final UUID accountId = account.getId(); final UUID bundleId = UUID.randomUUID(); final LocalDate targetDate1 = new LocalDate(2011, 10, 6); - final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD); + final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD); invoiceUtil.createInvoice(invoice1, true, context); final LocalDate startDate = new LocalDate(2011, 3, 1); @@ -585,7 +587,7 @@ public void testAccountBalanceWithNoInvoiceItems() throws EntityPersistenceExcep invoiceUtil.createInvoice(invoice1, true, context); final BigDecimal payment1 = new BigDecimal("48.0"); - final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD); + final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, true); invoiceUtil.createPayment(payment, context); final BigDecimal balance = invoiceDao.getAccountBalance(accountId, context); @@ -626,7 +628,7 @@ private void testAccountBalanceWithRefundInternal(final boolean withAdjustment) // Pay the whole thing final UUID paymentId = UUID.randomUUID(); final BigDecimal payment1 = rate1; - final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD); + final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, true); invoiceUtil.createPayment(payment, context); balance = invoiceDao.getAccountBalance(accountId, context); assertEquals(balance.compareTo(new BigDecimal("0.00")), 0); @@ -675,7 +677,7 @@ private void testRefundWithRepairAndInvoiceItemAdjustmentInternal(final BigDecim // Pay the whole thing final UUID paymentId = UUID.randomUUID(); final BigDecimal payment1 = amount; - final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), new DateTime(), payment1, Currency.USD, Currency.USD); + final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, true); invoiceUtil.createPayment(payment, context); balancePriorRefund = invoiceDao.getAccountBalance(accountId, context); assertEquals(balancePriorRefund.compareTo(new BigDecimal("0.00")), 0); @@ -780,7 +782,7 @@ private void testAccountBalanceWithRefundAndCBAInternal(final boolean withAdjust // Pay the whole thing final UUID paymentId = UUID.randomUUID(); final BigDecimal payment1 = amount1.add(rate1); - final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD); + final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, true); invoiceUtil.createPayment(payment, context); balance = invoiceDao.getAccountBalance(accountId, context); assertEquals(balance.compareTo(new BigDecimal("0.00")), 0); @@ -871,7 +873,7 @@ public void testAccountBalanceWithAllSortsOfThings() throws EntityPersistenceExc // Pay the whole thing final BigDecimal payment1 = amount1.add(rate1); - final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD); + final InvoicePayment payment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), new DateTime(), payment1, Currency.USD, Currency.USD, true); invoiceUtil.createPayment(payment, context); balance = invoiceDao.getAccountBalance(accountId, context); assertEquals(balance.compareTo(new BigDecimal("0.00")), 0); @@ -1081,6 +1083,7 @@ public void testGetUnpaidInvoicesByAccountId() throws EntityPersistenceException assertEquals(invoices.size(), 2); } + /* * * this test verifies that immediate changes give the correct results @@ -1109,7 +1112,8 @@ public void testInvoiceGenerationForImmediateChanges() throws InvoiceApiExceptio final BillingEventSet events = new MockBillingEventSet(); events.add(event1); - final Invoice invoice1 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, context); + final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, context); + final Invoice invoice1 = invoiceWithMetadata1.getInvoice(); assertEquals(invoice1.getBalance(), KillBillMoney.of(TEN, invoice1.getCurrency())); invoiceList.add(invoice1); @@ -1127,7 +1131,9 @@ public void testInvoiceGenerationForImmediateChanges() throws InvoiceApiExceptio // second invoice should be for one half (14/28 days) the difference between the rate plans // this is a temporary state, since it actually contains an adjusting item that properly belong to invoice 1 - final Invoice invoice2 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, context); + final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, context); + final Invoice invoice2 = invoiceWithMetadata2.getInvoice(); + assertEquals(invoice2.getBalance(), KillBillMoney.of(FIVE, invoice2.getCurrency())); invoiceList.add(invoice2); @@ -1145,25 +1151,24 @@ public void testInvoiceGenerationForImmediateChanges() throws InvoiceApiExceptio public void testInvoiceForFreeTrial() throws InvoiceApiException, CatalogApiException { final Currency currency = Currency.USD; final DefaultPrice price = new DefaultPrice(BigDecimal.ZERO, Currency.USD); - final MockInternationalPrice recurringPrice = new MockInternationalPrice(price); - final MockPlanPhase phase = new MockPlanPhase(recurringPrice, null); + final MockInternationalPrice fixedPrice = new MockInternationalPrice(price); + final MockPlanPhase phase = new MockPlanPhase(null, fixedPrice); final MockPlan plan = new MockPlan(phase); final SubscriptionBase subscription = getZombieSubscription(); final DateTime effectiveDate = invoiceUtil.buildDate(2011, 1, 1).toDateTimeAtStartOfDay(); - final BillingEvent event = invoiceUtil.createMockBillingEvent(null, subscription, effectiveDate, plan, phase, null, - recurringPrice.getPrice(currency), currency, BillingPeriod.MONTHLY, 15, BillingMode.IN_ADVANCE, + final BillingEvent event = invoiceUtil.createMockBillingEvent(null, subscription, effectiveDate, plan, phase, + fixedPrice.getPrice(currency), null, currency, BillingPeriod.MONTHLY, 15, BillingMode.IN_ADVANCE, "testEvent", 1L, SubscriptionBaseTransitionType.CREATE); final BillingEventSet events = new MockBillingEventSet(); events.add(event); final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 15); - final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, context); + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, context); + final Invoice invoice = invoiceWithMetadata.getInvoice(); + assertNotNull(invoice); - // expect one pro-ration item and one full-period item - assertEquals(invoice.getNumberOfItems(), 2); - assertEquals(invoice.getBalance().compareTo(ZERO), 0); } private SubscriptionBase getZombieSubscription(UUID subscriptionId) { @@ -1202,7 +1207,8 @@ public void testInvoiceForFreeTrialWithRecurringDiscount() throws InvoiceApiExce events.add(event1); final UUID accountId = account.getId(); - final Invoice invoice1 = generator.generateInvoice(account, events, null, new LocalDate(effectiveDate1), Currency.USD, context); + final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, new LocalDate(effectiveDate1), Currency.USD, context); + final Invoice invoice1 = invoiceWithMetadata1.getInvoice(); assertNotNull(invoice1); assertEquals(invoice1.getNumberOfItems(), 1); assertEquals(invoice1.getBalance().compareTo(ZERO), 0); @@ -1218,7 +1224,8 @@ public void testInvoiceForFreeTrialWithRecurringDiscount() throws InvoiceApiExce "testEvent2", 2L, SubscriptionBaseTransitionType.PHASE); events.add(event2); - final Invoice invoice2 = generator.generateInvoice(account, events, invoiceList, new LocalDate(effectiveDate2), Currency.USD, context); + final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, new LocalDate(effectiveDate2), Currency.USD, context); + final Invoice invoice2 = invoiceWithMetadata2.getInvoice(); assertNotNull(invoice2); assertEquals(invoice2.getNumberOfItems(), 1); assertEquals(invoice2.getBalance().compareTo(cheapAmount), 0); @@ -1228,7 +1235,8 @@ public void testInvoiceForFreeTrialWithRecurringDiscount() throws InvoiceApiExce //invoiceUtil.createInvoice(invoice2, invoice2.getTargetDate().getDayOfMonth(), callcontext); final DateTime effectiveDate3 = effectiveDate2.plusMonths(1); - final Invoice invoice3 = generator.generateInvoice(account, events, invoiceList, new LocalDate(effectiveDate3), Currency.USD, context); + final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, events, invoiceList, new LocalDate(effectiveDate3), Currency.USD, context); + final Invoice invoice3 = invoiceWithMetadata3.getInvoice(); assertNotNull(invoice3); assertEquals(invoice3.getNumberOfItems(), 1); assertEquals(invoice3.getBalance().compareTo(cheapAmount), 0); @@ -1239,7 +1247,8 @@ public void testInvoiceForFreeTrialWithRecurringDiscount() throws InvoiceApiExce @Test(groups = "slow") public void testInvoiceForEmptyEventSet() throws InvoiceApiException { final BillingEventSet events = new MockBillingEventSet(); - final Invoice invoice = generator.generateInvoice(account, events, null, new LocalDate(), Currency.USD, context); + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, new LocalDate(), Currency.USD, context); + final Invoice invoice = invoiceWithMetadata.getInvoice(); assertNull(invoice); } @@ -1273,7 +1282,8 @@ public void testMixedModeInvoicePersistence() throws InvoiceApiException, Catalo "testEvent2", 2L, SubscriptionBaseTransitionType.CHANGE); events.add(event2); - final Invoice invoice = generator.generateInvoice(account, events, null, new LocalDate(effectiveDate2), Currency.USD, context); + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, new LocalDate(effectiveDate2), Currency.USD, context); + final Invoice invoice = invoiceWithMetadata.getInvoice(); assertNotNull(invoice); assertEquals(invoice.getNumberOfItems(), 2); assertEquals(invoice.getBalance().compareTo(cheapAmount), 0); @@ -1314,7 +1324,7 @@ public void testRefundedInvoiceWithInvoiceItemAdjustmentWithRepair() throws Invo // SECOND CREATE THE PAYMENT final BigDecimal paymentAmount = new BigDecimal("239.00"); final UUID paymentId = UUID.randomUUID(); - final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, clock.getUTCNow(), paymentAmount, Currency.USD, Currency.USD); + final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, clock.getUTCNow(), paymentAmount, Currency.USD, Currency.USD, true); invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context); // AND THEN THIRD THE REFUND @@ -1344,7 +1354,8 @@ public void testRefundedInvoiceWithInvoiceItemAdjustmentWithRepair() throws Invo BillingPeriod.MONTHLY, 31, BillingMode.IN_ADVANCE, "new-event", 1L, SubscriptionBaseTransitionType.CREATE); events.add(event1); - final Invoice newInvoice = generator.generateInvoice(account, events, invoices, targetDate, Currency.USD, context); + final InvoiceWithMetadata newInvoiceWithMetadata = generator.generateInvoice(account, events, invoices, targetDate, Currency.USD, context); + final Invoice newInvoice = newInvoiceWithMetadata.getInvoice(); invoiceUtil.createInvoice(newInvoice, true, context); // VERIFY THAT WE STILL HAVE ONLY 2 ITEMS, MEANING THERE WERE NO REPAIR AND NO CBA GENERATED @@ -1379,7 +1390,8 @@ public void testInvoiceNumber() throws InvoiceApiException { "testEvent1", 1L, SubscriptionBaseTransitionType.CHANGE); events.add(event1); - Invoice invoice1 = generator.generateInvoice(account, events, invoices, new LocalDate(targetDate1), Currency.USD, context); + InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, invoices, new LocalDate(targetDate1), Currency.USD, context); + Invoice invoice1 = invoiceWithMetadata1.getInvoice(); invoices.add(invoice1); invoiceUtil.createInvoice(invoice1, true, context); invoice1 = new DefaultInvoice(invoiceDao.getById(invoice1.getId(), context)); @@ -1390,7 +1402,8 @@ public void testInvoiceNumber() throws InvoiceApiException { BillingPeriod.MONTHLY, 31, BillingMode.IN_ADVANCE, "testEvent2", 2L, SubscriptionBaseTransitionType.CHANGE); events.add(event2); - Invoice invoice2 = generator.generateInvoice(account, events, invoices, new LocalDate(targetDate2), Currency.USD, context); + InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoices, new LocalDate(targetDate2), Currency.USD, context); + Invoice invoice2 = invoiceWithMetadata2.getInvoice(); invoiceUtil.createInvoice(invoice2, true, context); invoice2 = new DefaultInvoice(invoiceDao.getById(invoice2.getId(), context)); assertNotNull(invoice2.getInvoiceNumber()); @@ -1456,7 +1469,7 @@ public void testRefundWithCBAPartiallyConsumed() throws Exception { final UUID paymentId = UUID.randomUUID(); final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), clock.getUTCNow().plusDays(12), new BigDecimal("10.0"), - Currency.USD, Currency.USD); + Currency.USD, Currency.USD, true); invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context); @@ -1522,7 +1535,7 @@ public void testRefundCBAFullyConsumedTwice() throws Exception { final UUID paymentId = UUID.randomUUID(); final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), clock.getUTCNow().plusDays(12), paymentAmount, - Currency.USD, Currency.USD); + Currency.USD, Currency.USD, true); invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context); // Create invoice 2 @@ -1605,6 +1618,40 @@ public void testCantDeleteCBAIfInvoiceBalanceBecomesNegative() throws Exception invoiceUtil.verifyInvoice(invoice1.getId(), 0.00, 10.00, context); } + + @Test(groups = "slow") + public void testWithFailedPaymentAttempt() throws Exception { + final UUID accountId = account.getId(); + final Invoice invoice = new DefaultInvoice(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD); + invoiceUtil.createInvoice(invoice, true, context); + + final UUID bundleId = UUID.randomUUID(); + final UUID subscriptionId = UUID.randomUUID(); + final RecurringInvoiceItem item1 = new RecurringInvoiceItem(invoice.getId(), accountId, bundleId, subscriptionId, "test plan", "test ZOO", clock.getUTCNow().plusMonths(-1).toLocalDate(), clock.getUTCNow().toLocalDate(), + BigDecimal.TEN, BigDecimal.TEN, Currency.USD); + invoiceUtil.createInvoiceItem(item1, context); + + final InvoiceModelDao retrievedInvoice = invoiceDao.getById(invoice.getId(), context); + assertEquals(retrievedInvoice.getInvoicePayments().size(), 0); + + + final UUID paymentId = UUID.randomUUID(); + final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), clock.getUTCNow().plusDays(12), BigDecimal.TEN, Currency.USD, Currency.USD, false); + invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context); + + final InvoiceModelDao retrievedInvoice1 = invoiceDao.getById(invoice.getId(), context); + assertEquals(retrievedInvoice1.getInvoicePayments().size(), 1); + assertEquals(retrievedInvoice1.getInvoicePayments().get(0).getSuccess(), Boolean.FALSE); + + final DefaultInvoicePayment defaultInvoicePayment2 = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), clock.getUTCNow().plusDays(12), BigDecimal.TEN, Currency.USD, Currency.USD, true); + invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment2), context); + + final InvoiceModelDao retrievedInvoice2 = invoiceDao.getById(invoice.getId(), context); + assertEquals(retrievedInvoice2.getInvoicePayments().size(), 1); + assertEquals(retrievedInvoice2.getInvoicePayments().get(0).getSuccess(), Boolean.TRUE); + } + + private void createCredit(final UUID accountId, final LocalDate effectiveDate, final BigDecimal creditAmount) { createCredit(accountId, null, effectiveDate, creditAmount); } diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java index 9094ddcb4d..4b8be241ac 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java @@ -33,6 +33,7 @@ import org.joda.time.DateTimeZone; import org.joda.time.LocalDate; import org.killbill.billing.account.api.Account; +import org.killbill.billing.account.api.DefaultAccount; import org.killbill.billing.catalog.DefaultPrice; import org.killbill.billing.catalog.MockInternationalPrice; import org.killbill.billing.catalog.MockPlan; @@ -127,8 +128,12 @@ public TimeSpan getDryRunNotificationSchedule() { public int getMaxRawUsagePreviousPeriod() { return -1; } + + @Override + public int getMaxGlobalLockRetries() { + return 10; + } }; - this.generator = new DefaultInvoiceGenerator(clock, invoiceConfig, null); this.account = new MockAccountBuilder().name(UUID.randomUUID().toString().substring(1, 8)) .firstNameLength(6) .email(UUID.randomUUID().toString().substring(1, 8)) @@ -146,15 +151,15 @@ public int getMaxRawUsagePreviousPeriod() { @Test(groups = "fast") public void testWithNullEventSetAndNullInvoiceSet() throws InvoiceApiException { - final Invoice invoice = generator.generateInvoice(account, null, null, clock.getUTCToday(), Currency.USD, internalCallContext); - assertNull(invoice); + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, null, null, clock.getUTCToday(), Currency.USD, internalCallContext); + assertNull(invoiceWithMetadata.getInvoice()); } @Test(groups = "fast") public void testWithEmptyEventSet() throws InvoiceApiException { final BillingEventSet events = new MockBillingEventSet(); - final Invoice invoice = generator.generateInvoice(account, events, null, clock.getUTCToday(), Currency.USD, internalCallContext); - assertNull(invoice); + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, clock.getUTCToday(), Currency.USD, internalCallContext); + assertNull(invoiceWithMetadata.getInvoice()); } @Test(groups = "fast") @@ -172,8 +177,8 @@ public void testWithSingleMonthlyEvent() throws InvoiceApiException, CatalogApiE events.add(event); final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3); - final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); - + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); + final Invoice invoice = invoiceWithMetadata.getInvoice(); assertNotNull(invoice); assertEquals(invoice.getNumberOfItems(), 2); assertEquals(invoice.getBalance(), KillBillMoney.of(TWENTY, invoice.getCurrency())); @@ -209,8 +214,8 @@ public void testSimpleWithTimeZone() throws InvoiceApiException, CatalogApiExcep // Target date is the next BCD, in local time final LocalDate targetDate = invoiceUtil.buildDate(2012, 8, bcdLocal); - final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); - + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); + final Invoice invoice = invoiceWithMetadata.getInvoice(); assertNotNull(invoice); assertEquals(invoice.getNumberOfItems(), 2); assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), invoiceUtil.buildDate(2012, 7, 16)); @@ -233,8 +238,8 @@ public void testSimpleWithSingleDiscountEvent() throws Exception { // Set a target date of today (start date) final LocalDate targetDate = startDate; - final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); - + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); + final Invoice invoice = invoiceWithMetadata.getInvoice(); assertNotNull(invoice); assertEquals(invoice.getNumberOfItems(), 1); assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), invoiceUtil.buildDate(2012, 7, 16)); @@ -255,8 +260,8 @@ public void testWithSingleMonthlyEventWithLeadingProRation() throws InvoiceApiEx events.add(event); final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3); - final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); - + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); + final Invoice invoice = invoiceWithMetadata.getInvoice(); assertNotNull(invoice); assertEquals(invoice.getNumberOfItems(), 2); @@ -287,8 +292,8 @@ public void testTwoMonthlySubscriptionsWithAlignedBillingDates() throws InvoiceA events.add(event2); final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3); - final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); - + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); + final Invoice invoice = invoiceWithMetadata.getInvoice(); assertNotNull(invoice); assertEquals(invoice.getNumberOfItems(), 2); assertEquals(invoice.getBalance(), KillBillMoney.of(rate1.add(rate2), invoice.getCurrency())); @@ -313,8 +318,8 @@ public void testOnePlan_TwoMonthlyPhases_ChangeImmediate() throws InvoiceApiExce final LocalDate targetDate = invoiceUtil.buildDate(2011, 12, 3); final UUID accountId = UUID.randomUUID(); - final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); - + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); + final Invoice invoice = invoiceWithMetadata.getInvoice(); assertNotNull(invoice); assertEquals(invoice.getNumberOfItems(), 4); @@ -354,8 +359,8 @@ public void testOnePlan_ThreeMonthlyPhases_ChangeEOT() throws InvoiceApiExceptio events.add(event3); final LocalDate targetDate = invoiceUtil.buildDate(2011, 12, 3); - final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); - + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); + final Invoice invoice = invoiceWithMetadata.getInvoice(); assertNotNull(invoice); assertEquals(invoice.getNumberOfItems(), 4); assertEquals(invoice.getBalance(), KillBillMoney.of(rate1.add(rate2).add(TWO.multiply(rate3)), invoice.getCurrency())); @@ -376,13 +381,14 @@ public void testSingleEventWithExistingInvoice() throws InvoiceApiException, Cat events.add(event1); LocalDate targetDate = invoiceUtil.buildDate(2011, 12, 1); - final Invoice invoice1 = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); final List existingInvoices = new ArrayList(); + final Invoice invoice1 = invoiceWithMetadata1.getInvoice(); existingInvoices.add(invoice1); targetDate = invoiceUtil.buildDate(2011, 12, 3); - final Invoice invoice2 = generator.generateInvoice(account, events, existingInvoices, targetDate, Currency.USD, internalCallContext); - + final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, existingInvoices, targetDate, Currency.USD, internalCallContext); + final Invoice invoice2 = invoiceWithMetadata2.getInvoice(); assertNull(invoice2); } @@ -554,24 +560,26 @@ public void testZeroDollarEvents() throws InvoiceApiException, CatalogApiExcepti final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 1); events.add(createBillingEvent(UUID.randomUUID(), UUID.randomUUID(), targetDate, plan, planPhase, 1)); - final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); - - assertEquals(invoice.getNumberOfItems(), 1); + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); + final Invoice invoice = invoiceWithMetadata.getInvoice(); + assertNull(invoice); } @Test(groups = "fast") public void testEndDateIsCorrect() throws InvoiceApiException, CatalogApiException { final Plan plan = new MockPlan(); - final PlanPhase planPhase = createMockMonthlyPlanPhase(ZERO); + final PlanPhase planPhase = createMockMonthlyPlanPhase(ONE); final BillingEventSet events = new MockBillingEventSet(); final LocalDate startDate = clock.getUTCToday().minusDays(1); final LocalDate targetDate = startDate.plusDays(1); events.add(createBillingEvent(UUID.randomUUID(), UUID.randomUUID(), startDate, plan, planPhase, startDate.getDayOfMonth())); - final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); + final Invoice invoice = invoiceWithMetadata.getInvoice(); final RecurringInvoiceItem item = (RecurringInvoiceItem) invoice.getInvoiceItems().get(0); + // end date of the invoice item should be equal to exactly one month later (rounded) assertEquals(item.getEndDate(), startDate.plusMonths(1)); } @@ -605,13 +613,15 @@ public void testFixedPriceLifeCycle() throws InvoiceApiException { events.add(event2); events.add(event1); - final Invoice invoice1 = generator.generateInvoice(account, events, null, new LocalDate("2012-02-01"), Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, new LocalDate("2012-02-01"), Currency.USD, internalCallContext); + final Invoice invoice1 = invoiceWithMetadata1.getInvoice(); assertNotNull(invoice1); assertEquals(invoice1.getNumberOfItems(), 1); final List invoiceList = new ArrayList(); invoiceList.add(invoice1); - final Invoice invoice2 = generator.generateInvoice(account, events, invoiceList, new LocalDate("2012-04-05"), Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, new LocalDate("2012-04-05"), Currency.USD, internalCallContext); + final Invoice invoice2 = invoiceWithMetadata2.getInvoice(); assertNotNull(invoice2); assertEquals(invoice2.getNumberOfItems(), 1); final FixedPriceInvoiceItem item = (FixedPriceInvoiceItem) invoice2.getInvoiceItems().get(0); @@ -635,7 +645,8 @@ public void testMixedModeLifeCycle() throws InvoiceApiException, CatalogApiExcep events.add(event1); // ensure both components are invoiced - final Invoice invoice1 = generator.generateInvoice(account, events, null, startDate, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, startDate, Currency.USD, internalCallContext); + final Invoice invoice1 = invoiceWithMetadata1.getInvoice(); assertNotNull(invoice1); assertEquals(invoice1.getNumberOfItems(), 2); assertEquals(invoice1.getBalance(), KillBillMoney.of(FIFTEEN, invoice1.getCurrency())); @@ -647,7 +658,8 @@ public void testMixedModeLifeCycle() throws InvoiceApiException, CatalogApiExcep final LocalDate currentDate = startDate.plusMonths(1); // ensure that only the recurring price is invoiced - final Invoice invoice2 = generator.generateInvoice(account, events, invoiceList, currentDate, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, currentDate, Currency.USD, internalCallContext); + final Invoice invoice2 = invoiceWithMetadata2.getInvoice(); assertNotNull(invoice2); assertEquals(invoice2.getNumberOfItems(), 1); assertEquals(invoice2.getBalance(), KillBillMoney.of(FIVE, invoice2.getCurrency())); @@ -672,8 +684,8 @@ public void testFixedModePlanChange() throws InvoiceApiException, CatalogApiExce events.add(event1); // ensure that a single invoice item is generated for the fixed cost - final Invoice invoice1 = generator.generateInvoice(account, events, null, startDate, Currency.USD, internalCallContext); - assertNotNull(invoice1); + final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, startDate, Currency.USD, internalCallContext); + final Invoice invoice1 = invoiceWithMetadata1.getInvoice();assertNotNull(invoice1); assertEquals(invoice1.getNumberOfItems(), 1); assertEquals(invoice1.getBalance(), KillBillMoney.of(fixedCost1, invoice1.getCurrency())); @@ -686,8 +698,8 @@ public void testFixedModePlanChange() throws InvoiceApiException, CatalogApiExce events.add(event2); // ensure that a single invoice item is generated for the fixed cost - final Invoice invoice2 = generator.generateInvoice(account, events, invoiceList, phaseChangeDate, Currency.USD, internalCallContext); - assertNotNull(invoice2); + final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, phaseChangeDate, Currency.USD, internalCallContext); + final Invoice invoice2 = invoiceWithMetadata2.getInvoice(); assertEquals(invoice2.getNumberOfItems(), 1); assertEquals(invoice2.getBalance(), KillBillMoney.of(fixedCost2, invoice2.getCurrency())); } @@ -718,7 +730,8 @@ public void testInvoiceGenerationFailureScenario() throws InvoiceApiException, C final LocalDate discountPhaseEndDate = trialPhaseEndDate.plusMonths(6); events.add(createBillingEvent(subscriptionId, bundleId, discountPhaseEndDate, plan1, phase3, BILL_CYCLE_DAY)); - final Invoice invoice1 = generator.generateInvoice(account, events, null, creationDate, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, creationDate, Currency.USD, internalCallContext); + final Invoice invoice1 = invoiceWithMetadata1.getInvoice(); assertNotNull(invoice1); assertEquals(invoice1.getNumberOfItems(), 1); assertEquals(invoice1.getBalance().compareTo(ZERO), 0); @@ -726,7 +739,8 @@ public void testInvoiceGenerationFailureScenario() throws InvoiceApiException, C final List invoiceList = new ArrayList(); invoiceList.add(invoice1); - final Invoice invoice2 = generator.generateInvoice(account, events, invoiceList, trialPhaseEndDate, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, trialPhaseEndDate, Currency.USD, internalCallContext); + final Invoice invoice2 = invoiceWithMetadata2.getInvoice(); assertNotNull(invoice2); assertEquals(invoice2.getNumberOfItems(), 1); assertEquals(invoice2.getInvoiceItems().get(0).getStartDate(), trialPhaseEndDate); @@ -734,7 +748,8 @@ public void testInvoiceGenerationFailureScenario() throws InvoiceApiException, C invoiceList.add(invoice2); LocalDate targetDate = new LocalDate(trialPhaseEndDate.getYear(), trialPhaseEndDate.getMonthOfYear(), BILL_CYCLE_DAY); - final Invoice invoice3 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, internalCallContext); + final Invoice invoice3 = invoiceWithMetadata3.getInvoice(); assertNotNull(invoice3); assertEquals(invoice3.getNumberOfItems(), 1); assertEquals(invoice3.getInvoiceItems().get(0).getStartDate(), targetDate); @@ -742,7 +757,8 @@ public void testInvoiceGenerationFailureScenario() throws InvoiceApiException, C invoiceList.add(invoice3); targetDate = targetDate.plusMonths(6); - final Invoice invoice4 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata4 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, internalCallContext); + final Invoice invoice4 = invoiceWithMetadata4.getInvoice(); assertNotNull(invoice4); assertEquals(invoice4.getNumberOfItems(), 7); } @@ -801,7 +817,8 @@ private void testInvoiceGeneration(final UUID accountId, final BillingEventSet e final LocalDate targetDate, final int expectedNumberOfItems, final BigDecimal expectedAmount) throws InvoiceApiException { final Currency currency = Currency.USD; - final Invoice invoice = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext); + final Invoice invoice = invoiceWithMetadata.getInvoice(); assertNotNull(invoice); assertEquals(invoice.getNumberOfItems(), expectedNumberOfItems); existingInvoices.add(invoice); @@ -827,7 +844,8 @@ public void testWithFullRepairInvoiceGeneration() throws CatalogApiException, In events.add(createBillingEvent(baseSubscription.getId(), baseSubscription.getBundleId(), april25, basePlan, basePlanEvergreen, 25)); // generate invoice - final Invoice invoice1 = generator.generateInvoice(account, events, null, april25, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, april25, Currency.USD, internalCallContext); + final Invoice invoice1 = invoiceWithMetadata1.getInvoice(); assertNotNull(invoice1); assertEquals(invoice1.getNumberOfItems(), 1); assertEquals(invoice1.getBalance().compareTo(TEN), 0); @@ -848,7 +866,9 @@ public void testWithFullRepairInvoiceGeneration() throws CatalogApiException, In events.add(createBillingEvent(addOnSubscription2.getId(), baseSubscription.getBundleId(), april28, addOn2Plan, addOn2PlanPhaseEvergreen, 25)); // generate invoice - final Invoice invoice2 = generator.generateInvoice(account, events, invoices, april28, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoices, april28, Currency.USD, internalCallContext); + final Invoice invoice2 = invoiceWithMetadata2.getInvoice(); + invoices.add(invoice2); assertNotNull(invoice2); assertEquals(invoice2.getNumberOfItems(), 2); @@ -865,7 +885,8 @@ public void testWithFullRepairInvoiceGeneration() throws CatalogApiException, In // generate invoice final LocalDate may1 = new LocalDate(2012, 5, 1); - final Invoice invoice3 = generator.generateInvoice(account, newEvents, invoices, may1, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, newEvents, invoices, may1, Currency.USD, internalCallContext); + final Invoice invoice3 = invoiceWithMetadata3.getInvoice(); assertNotNull(invoice3); assertEquals(invoice3.getNumberOfItems(), 3); // -4.50 -18 - 10 (to correct the previous 2 invoices) + 4.50 + 13 @@ -887,7 +908,8 @@ public void testRepairForPaidInvoice() throws CatalogApiException, InvoiceApiExc final BillingEventSet events = new MockBillingEventSet(); events.add(createBillingEvent(originalSubscription.getId(), originalSubscription.getBundleId(), april25, originalPlan, originalPlanEvergreen, 25)); - final Invoice invoice1 = generator.generateInvoice(account, events, null, april25, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, april25, Currency.USD, internalCallContext); + final Invoice invoice1 = invoiceWithMetadata1.getInvoice(); printDetailInvoice(invoice1); @@ -897,7 +919,7 @@ public void testRepairForPaidInvoice() throws CatalogApiException, InvoiceApiExc // pay the invoice invoice1.addPayment(new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice1.getId(), april25.toDateTimeAtCurrentTime(), TEN, - Currency.USD, Currency.USD)); + Currency.USD, Currency.USD, true)); assertEquals(invoice1.getBalance().compareTo(ZERO), 0); // change the plan (i.e. repair) on start date @@ -909,7 +931,8 @@ public void testRepairForPaidInvoice() throws CatalogApiException, InvoiceApiExc events.add(createBillingEvent(newSubscription.getId(), originalSubscription.getBundleId(), april25, newPlan, newPlanEvergreen, 25)); // generate a new invoice - final Invoice invoice2 = generator.generateInvoice(account, events, invoices, april25, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoices, april25, Currency.USD, internalCallContext); + final Invoice invoice2 = invoiceWithMetadata2.getInvoice(); printDetailInvoice(invoice2); assertEquals(invoice2.getNumberOfItems(), 2); @@ -973,7 +996,8 @@ public void testRegressionFor170() throws EntityPersistenceException, InvoiceApi // Generate a new invoice - final Invoice invoice = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext); + final Invoice invoice = invoiceWithMetadata.getInvoice(); assertEquals(invoice.getNumberOfItems(), 7); assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING); assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2013, 6, 15)); @@ -1007,7 +1031,8 @@ public void testRegressionFor170() throws EntityPersistenceException, InvoiceApi existingInvoices.add(invoice); // Generate next invoice (no-op) - final Invoice newInvoice = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext); + final InvoiceWithMetadata newInvoiceWithMetdata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext); + final Invoice newInvoice = newInvoiceWithMetdata.getInvoice(); assertNull(newInvoice); } @@ -1054,9 +1079,9 @@ public void testAutoInvoiceOffAccount() throws Exception { final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3); final UUID accountId = UUID.randomUUID(); - final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext); - assertNull(invoice); + assertNull(invoiceWithMetadata.getInvoice()); } public void testAutoInvoiceOffWithCredits() throws CatalogApiException, InvoiceApiException { @@ -1082,7 +1107,8 @@ public void testAutoInvoiceOffWithCredits() throws CatalogApiException, InvoiceA eventSet.add(createBillingEvent(subscriptionId2, bundleId, startDate, plan2, plan2phase1, 1)); // generate the first invoice - final Invoice invoice1 = generator.generateInvoice(account, eventSet, invoices, startDate, currency, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, eventSet, invoices, startDate, currency, internalCallContext); + final Invoice invoice1 = invoiceWithMetadata1.getInvoice(); assertNotNull(invoice1); assertTrue(invoice1.getBalance().compareTo(FIFTEEN.add(TWELVE)) == 0); invoices.add(invoice1); @@ -1093,7 +1119,8 @@ public void testAutoInvoiceOffWithCredits() throws CatalogApiException, InvoiceA eventSet.addSubscriptionWithAutoInvoiceOff(subscriptionId1); final LocalDate targetDate2 = startDate.plusMonths(1); - final Invoice invoice2 = generator.generateInvoice(account, eventSet, invoices, targetDate2, currency, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, eventSet, invoices, targetDate2, currency, internalCallContext); + final Invoice invoice2 = invoiceWithMetadata2.getInvoice(); assertNotNull(invoice2); assertTrue(invoice2.getBalance().compareTo(TWELVE) == 0); invoices.add(invoice2); @@ -1101,7 +1128,8 @@ public void testAutoInvoiceOffWithCredits() throws CatalogApiException, InvoiceA final LocalDate targetDate3 = targetDate2.plusMonths(1); eventSet.clearSubscriptionsWithAutoInvoiceOff(); eventSet.add(subscription1creation); - final Invoice invoice3 = generator.generateInvoice(account, eventSet, invoices, targetDate3, currency, internalCallContext); + final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, eventSet, invoices, targetDate3, currency, internalCallContext); + final Invoice invoice3 = invoiceWithMetadata3.getInvoice(); assertNotNull(invoice3); assertTrue(invoice3.getBalance().compareTo(FIFTEEN.multiply(TWO).add(TWELVE)) == 0); } diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestBillingIntervalDetail.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInAdvanceBillingIntervalDetail.java similarity index 92% rename from invoice/src/test/java/org/killbill/billing/invoice/generator/TestBillingIntervalDetail.java rename to invoice/src/test/java/org/killbill/billing/invoice/generator/TestInAdvanceBillingIntervalDetail.java index 9941eb98f5..2328f56657 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestBillingIntervalDetail.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInAdvanceBillingIntervalDetail.java @@ -17,13 +17,14 @@ package org.killbill.billing.invoice.generator; import org.joda.time.LocalDate; +import org.killbill.billing.catalog.api.BillingMode; import org.testng.Assert; import org.testng.annotations.Test; import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.InvoiceTestSuiteNoDB; -public class TestBillingIntervalDetail extends InvoiceTestSuiteNoDB { +public class TestInAdvanceBillingIntervalDetail extends InvoiceTestSuiteNoDB { /* * @@ -35,7 +36,7 @@ public class TestBillingIntervalDetail extends InvoiceTestSuiteNoDB { public void testCalculateFirstBillingCycleDate1() throws Exception { final LocalDate from = new LocalDate("2012-01-16"); final int bcd = 17; - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.ANNUAL); + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.ANNUAL, BillingMode.IN_ADVANCE); billingIntervalDetail.calculateFirstBillingCycleDate(); Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-01-17")); } @@ -50,7 +51,7 @@ public void testCalculateFirstBillingCycleDate1() throws Exception { public void testCalculateFirstBillingCycleDate2() throws Exception { final LocalDate from = new LocalDate("2012-02-16"); final int bcd = 30; - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.ANNUAL); + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.ANNUAL, BillingMode.IN_ADVANCE); billingIntervalDetail.calculateFirstBillingCycleDate(); Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-02-29")); } @@ -69,7 +70,7 @@ public void testCalculateFirstBillingCycleDate2() throws Exception { public void testCalculateFirstBillingCycleDate4() throws Exception { final LocalDate from = new LocalDate("2012-01-31"); final int bcd = 30; - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.MONTHLY); + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.MONTHLY, BillingMode.IN_ADVANCE); billingIntervalDetail.calculateFirstBillingCycleDate(); Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-02-29")); } @@ -84,7 +85,7 @@ public void testCalculateFirstBillingCycleDate4() throws Exception { public void testCalculateFirstBillingCycleDate3() throws Exception { final LocalDate from = new LocalDate("2012-02-16"); final int bcd = 14; - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.ANNUAL); + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.ANNUAL, BillingMode.IN_ADVANCE); billingIntervalDetail.calculateFirstBillingCycleDate(); Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2013-02-14")); } @@ -92,7 +93,7 @@ public void testCalculateFirstBillingCycleDate3() throws Exception { @Test(groups = "fast") public void testNextBCDShouldNotBeInThePast() throws Exception { final LocalDate from = new LocalDate("2012-07-16"); - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), 15, BillingPeriod.MONTHLY); + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), 15, BillingPeriod.MONTHLY, BillingMode.IN_ADVANCE); final LocalDate to = billingIntervalDetail.getFirstBillingCycleDate(); Assert.assertEquals(to, new LocalDate("2012-08-15")); } @@ -100,7 +101,7 @@ public void testNextBCDShouldNotBeInThePast() throws Exception { @Test(groups = "fast") public void testBeforeBCDWithOnOrAfter() throws Exception { final LocalDate from = new LocalDate("2012-03-02"); - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), 3, BillingPeriod.MONTHLY); + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), 3, BillingPeriod.MONTHLY, BillingMode.IN_ADVANCE); final LocalDate to = billingIntervalDetail.getFirstBillingCycleDate(); Assert.assertEquals(to, new LocalDate("2012-03-03")); } @@ -108,7 +109,7 @@ public void testBeforeBCDWithOnOrAfter() throws Exception { @Test(groups = "fast") public void testEqualBCDWithOnOrAfter() throws Exception { final LocalDate from = new LocalDate("2012-03-03"); - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), 3, BillingPeriod.MONTHLY); + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), 3, BillingPeriod.MONTHLY, BillingMode.IN_ADVANCE); final LocalDate to = billingIntervalDetail.getFirstBillingCycleDate(); Assert.assertEquals(to, new LocalDate("2012-03-03")); } @@ -116,7 +117,7 @@ public void testEqualBCDWithOnOrAfter() throws Exception { @Test(groups = "fast") public void testAfterBCDWithOnOrAfter() throws Exception { final LocalDate from = new LocalDate("2012-03-04"); - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), 3, BillingPeriod.MONTHLY); + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), 3, BillingPeriod.MONTHLY, BillingMode.IN_ADVANCE); final LocalDate to = billingIntervalDetail.getFirstBillingCycleDate(); Assert.assertEquals(to, new LocalDate("2012-04-03")); } @@ -127,7 +128,7 @@ public void testEffectiveEndDate() throws Exception { final LocalDate targetDate = new LocalDate(2012, 8, 16); final BillingPeriod billingPeriod = BillingPeriod.MONTHLY; - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(firstBCD, null, targetDate, 16, billingPeriod); + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(firstBCD, null, targetDate, 16, billingPeriod, BillingMode.IN_ADVANCE); final LocalDate effectiveEndDate = billingIntervalDetail.getEffectiveEndDate(); Assert.assertEquals(effectiveEndDate, new LocalDate(2012, 9, 16)); } @@ -138,7 +139,7 @@ public void testLastBCD() throws Exception { final LocalDate endDate = new LocalDate(2012, 9, 15); // so we get effectiveEndDate on 9/15 final LocalDate targetDate = new LocalDate(2012, 8, 16); - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, endDate, targetDate, 16, BillingPeriod.MONTHLY); + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, endDate, targetDate, 16, BillingPeriod.MONTHLY, BillingMode.IN_ADVANCE); final LocalDate lastBCD = billingIntervalDetail.getLastBillingCycleDate(); Assert.assertEquals(lastBCD, new LocalDate(2012, 8, 16)); } @@ -148,7 +149,7 @@ public void testLastBCDShouldNotBeBeforePreviousBCD() throws Exception { final LocalDate start = new LocalDate("2012-07-16"); final int bcdLocal = 15; - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, null, start, bcdLocal, BillingPeriod.MONTHLY); + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, null, start, bcdLocal, BillingPeriod.MONTHLY, BillingMode.IN_ADVANCE); final LocalDate lastBCD = billingIntervalDetail.getLastBillingCycleDate(); Assert.assertEquals(lastBCD, new LocalDate("2012-08-15")); } @@ -160,7 +161,7 @@ public void testBCD31StartingWith30DayMonth() throws Exception { final LocalDate end = null; final int bcdLocal = 31; - final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, end, targetDate, bcdLocal, BillingPeriod.MONTHLY); + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, end, targetDate, bcdLocal, BillingPeriod.MONTHLY, BillingMode.IN_ADVANCE); final LocalDate effectiveEndDate = billingIntervalDetail.getEffectiveEndDate(); Assert.assertEquals(effectiveEndDate, new LocalDate("2012-05-31")); } diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInArrearBillingIntervalDetail.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInArrearBillingIntervalDetail.java new file mode 100644 index 0000000000..ef93815d41 --- /dev/null +++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInArrearBillingIntervalDetail.java @@ -0,0 +1,248 @@ +/* + * 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 org.joda.time.LocalDate; +import org.killbill.billing.catalog.api.BillingMode; +import org.killbill.billing.catalog.api.BillingPeriod; +import org.killbill.billing.invoice.InvoiceTestSuiteNoDB; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class TestInArrearBillingIntervalDetail extends InvoiceTestSuiteNoDB { + + + /* + * TD + * BCD Start + * |---------|----------------- + * + */ + @Test(groups = "fast") + public void testScenarioBCDBeforeStart1() throws Exception { + final LocalDate start = new LocalDate("2012-01-16"); + final LocalDate targetDate = new LocalDate("2012-01-16"); + final int bcd = 13; + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, null, targetDate, bcd, BillingPeriod.MONTHLY, BillingMode.IN_ARREAR); + + Assert.assertFalse(billingIntervalDetail.hasSomethingToBill()); + Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-02-13")); + Assert.assertNull(billingIntervalDetail.getEffectiveEndDate()); + Assert.assertEquals(billingIntervalDetail.getNextBillingCycleDate(), new LocalDate("2012-02-13")); + } + + /* + * + * BCD Start TD = (next BCD) + * |---------|----------|------- + * + */ + @Test(groups = "fast") + public void testScenarioBCDBeforeStart2() throws Exception { + final LocalDate start = new LocalDate("2012-01-16"); + final LocalDate targetDate = new LocalDate("2012-02-13"); + final int bcd = 13; + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, null, targetDate, bcd, BillingPeriod.MONTHLY, BillingMode.IN_ARREAR); + + Assert.assertTrue(billingIntervalDetail.hasSomethingToBill()); + Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-02-13")); + Assert.assertEquals(billingIntervalDetail.getEffectiveEndDate(), new LocalDate("2012-02-13")); + Assert.assertEquals(billingIntervalDetail.getNextBillingCycleDate(), new LocalDate("2012-03-13")); + } + + + /* + * BCD + * Start TD + * |---------|----------------- + * + */ + @Test(groups = "fast") + public void testScenarioBCDAEqualsStart1() throws Exception { + final LocalDate start = new LocalDate("2012-01-16"); + final LocalDate targetDate = new LocalDate("2012-01-19"); + final int bcd = 16; + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, null, targetDate, bcd, BillingPeriod.MONTHLY, BillingMode.IN_ARREAR); + + Assert.assertTrue(billingIntervalDetail.hasSomethingToBill()); + Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-01-16")); + Assert.assertEquals(billingIntervalDetail.getEffectiveEndDate(), new LocalDate("2012-01-16")); + Assert.assertEquals(billingIntervalDetail.getNextBillingCycleDate(), new LocalDate("2012-02-16")); + } + + + /* + * + * Start TD BCD + * |---------|----------|------- + * + */ + @Test(groups = "fast") + public void testScenarioBCDAfterStart1() throws Exception { + final LocalDate start = new LocalDate("2012-01-16"); + final LocalDate targetDate = new LocalDate("2012-01-19"); + final int bcd = 25; + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, null, targetDate, bcd, BillingPeriod.MONTHLY, BillingMode.IN_ARREAR); + + Assert.assertFalse(billingIntervalDetail.hasSomethingToBill()); + Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-01-25")); + Assert.assertNull(billingIntervalDetail.getEffectiveEndDate()); + Assert.assertEquals(billingIntervalDetail.getNextBillingCycleDate(), new LocalDate("2012-01-25")); + } + + + /* + * TD + * Start End BCD + * |---------|------------|------- + * + */ + @Test(groups = "fast") + public void testScenarioBCDAfterStart2() throws Exception { + final LocalDate start = new LocalDate("2012-01-16"); + final LocalDate end = new LocalDate("2012-01-19"); + final LocalDate targetDate = new LocalDate("2012-01-25"); + final int bcd = 25; + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, end, targetDate, bcd, BillingPeriod.MONTHLY, BillingMode.IN_ARREAR); + + Assert.assertTrue(billingIntervalDetail.hasSomethingToBill()); + Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-01-25")); + Assert.assertEquals(billingIntervalDetail.getEffectiveEndDate(), end); + // STEPH maybe we should change because we actually don't want a notification + Assert.assertEquals(billingIntervalDetail.getNextBillingCycleDate(), new LocalDate("2012-01-25")); + } + + + /* + * TD + * Start BCD + * |--------------------|------- + * + */ + @Test(groups = "fast") + public void testScenarioBCDAfterStart3() throws Exception { + final LocalDate start = new LocalDate("2012-01-16"); + final LocalDate targetDate = new LocalDate("2012-01-25"); + final int bcd = 25; + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, null, targetDate, bcd, BillingPeriod.MONTHLY, BillingMode.IN_ARREAR); + + Assert.assertTrue(billingIntervalDetail.hasSomethingToBill()); + Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-01-25")); + Assert.assertEquals(billingIntervalDetail.getEffectiveEndDate(), new LocalDate("2012-01-25")); + Assert.assertEquals(billingIntervalDetail.getNextBillingCycleDate(), new LocalDate("2012-02-25")); + } + + + /* + * + * Start BCD end TD next BCD + * |-------|------|----|------|--- + * + */ + @Test(groups = "fast") + public void testScenarioEndDateBetweenPeriod1() throws Exception { + final LocalDate start = new LocalDate("2012-01-16"); + final LocalDate end = new LocalDate("2012-01-20"); + final LocalDate targetDate = new LocalDate("2012-01-25"); + final int bcd = 18; + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, end, targetDate, bcd, BillingPeriod.MONTHLY, BillingMode.IN_ARREAR); + + Assert.assertTrue(billingIntervalDetail.hasSomethingToBill()); + Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-01-18")); + Assert.assertEquals(billingIntervalDetail.getEffectiveEndDate(), new LocalDate("2012-01-20")); + Assert.assertEquals(billingIntervalDetail.getNextBillingCycleDate(), new LocalDate("2012-02-18")); + } + + /* + * + * Start BCD TD next BCD + * |-------|----------|------|--- + * + */ + @Test(groups = "fast") + public void testScenarioEndDateBetweenPeriod2() throws Exception { + final LocalDate start = new LocalDate("2012-01-16"); + final LocalDate targetDate = new LocalDate("2012-01-25"); + final int bcd = 18; + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, null, targetDate, bcd, BillingPeriod.MONTHLY, BillingMode.IN_ARREAR); + + Assert.assertTrue(billingIntervalDetail.hasSomethingToBill()); + Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-01-18")); + Assert.assertEquals(billingIntervalDetail.getEffectiveEndDate(), new LocalDate("2012-01-18")); + Assert.assertEquals(billingIntervalDetail.getNextBillingCycleDate(), new LocalDate("2012-02-18")); + } + + /* + * + * Start BCD TD end next BCD + * |-------|----------|----|------|--- + * + */ + @Test(groups = "fast") + public void testScenarioEndDateBetweenPeriod3() throws Exception { + final LocalDate start = new LocalDate("2012-01-16"); + final LocalDate end = new LocalDate("2012-01-28"); + final LocalDate targetDate = new LocalDate("2012-01-25"); + final int bcd = 18; + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, end, targetDate, bcd, BillingPeriod.MONTHLY, BillingMode.IN_ARREAR); + + Assert.assertTrue(billingIntervalDetail.hasSomethingToBill()); + Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-01-18")); + Assert.assertEquals(billingIntervalDetail.getEffectiveEndDate(), new LocalDate("2012-01-18")); + Assert.assertEquals(billingIntervalDetail.getNextBillingCycleDate(), new LocalDate("2012-02-18")); + } + + /* + * TD + * Start BCD next BCD + * |-------|--------------------|--- + * + */ + @Test(groups = "fast") + public void testScenarioTargetDateOnNextBCD1() throws Exception { + final LocalDate start = new LocalDate("2012-01-16"); + final LocalDate targetDate = new LocalDate("2012-02-18"); + final int bcd = 18; + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, null, targetDate, bcd, BillingPeriod.MONTHLY, BillingMode.IN_ARREAR); + + Assert.assertTrue(billingIntervalDetail.hasSomethingToBill()); + Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-01-18")); + Assert.assertEquals(billingIntervalDetail.getEffectiveEndDate(), new LocalDate("2012-02-18")); + Assert.assertEquals(billingIntervalDetail.getNextBillingCycleDate(), new LocalDate("2012-03-18")); + } + + /* + * TD + * Start BCD end next BCD + * |-------|-------------|-------|--- + * + */ + @Test(groups = "fast") + public void testScenarioTargetDateOnNextBCD2() throws Exception { + final LocalDate start = new LocalDate("2012-01-16"); + final LocalDate end = new LocalDate("2012-02-16"); + final LocalDate targetDate = new LocalDate("2012-02-18"); + final int bcd = 18; + final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, end, targetDate, bcd, BillingPeriod.MONTHLY, BillingMode.IN_ARREAR); + + Assert.assertTrue(billingIntervalDetail.hasSomethingToBill()); + Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-01-18")); + Assert.assertEquals(billingIntervalDetail.getEffectiveEndDate(), new LocalDate("2012-02-16")); + Assert.assertEquals(billingIntervalDetail.getNextBillingCycleDate(), new LocalDate("2012-02-18")); + } +} diff --git a/invoice/src/test/java/org/killbill/billing/invoice/model/TestInAdvanceBillingMode.java b/invoice/src/test/java/org/killbill/billing/invoice/model/TestRecurringInAdvance.java similarity index 82% rename from invoice/src/test/java/org/killbill/billing/invoice/model/TestInAdvanceBillingMode.java rename to invoice/src/test/java/org/killbill/billing/invoice/model/TestRecurringInAdvance.java index c5c35b90f6..534b3f84ee 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/model/TestInAdvanceBillingMode.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/model/TestRecurringInAdvance.java @@ -23,13 +23,14 @@ import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.LocalDate; +import org.killbill.billing.catalog.api.BillingMode; import org.testng.Assert; import org.testng.annotations.Test; import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.InvoiceTestSuiteNoDB; -public class TestInAdvanceBillingMode extends InvoiceTestSuiteNoDB { +public class TestRecurringInAdvance extends InvoiceTestSuiteNoDB { private static final DateTimeZone TIMEZONE = DateTimeZone.forID("Pacific/Pitcairn"); private static final BillingPeriod BILLING_PERIOD = BillingPeriod.MONTHLY; @@ -42,7 +43,7 @@ public void testItemShouldNotStartInThePast() throws Exception { final int billingCycleDayLocal = 15; final LinkedHashMap expectedDates = new LinkedHashMap(); - verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); } @Test(groups = "fast") @@ -55,7 +56,7 @@ public void testCalculateSimpleInvoiceItemWithNoEndDate() throws Exception { final LinkedHashMap expectedDates = new LinkedHashMap(); expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 15)); - verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); } @Test(groups = "fast") @@ -68,7 +69,7 @@ public void testCalculateSimpleInvoiceItemWithBCDBeforeStartDay() throws Excepti final LinkedHashMap expectedDates = new LinkedHashMap(); expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 15)); - verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); } @Test(groups = "fast") @@ -81,7 +82,7 @@ public void testCalculateSimpleInvoiceItemWithBCDEqualsStartDay() throws Excepti final LinkedHashMap expectedDates = new LinkedHashMap(); expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 16)); - verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); } @Test(groups = "fast") @@ -94,7 +95,7 @@ public void testCalculateSimpleInvoiceItemWithBCDAfterStartDay() throws Exceptio final LinkedHashMap expectedDates = new LinkedHashMap(); expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 7, 17)); - verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); } @Test(groups = "fast") @@ -110,7 +111,7 @@ public void testCalculateSimpleInvoiceItemWithBCDBeforeStartDayWithTargetDateIn3 expectedDates.put(new LocalDate(2012, 9, 15), new LocalDate(2012, 10, 15)); expectedDates.put(new LocalDate(2012, 10, 15), new LocalDate(2012, 11, 15)); - verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); } @Test(groups = "fast") @@ -126,7 +127,7 @@ public void testCalculateSimpleInvoiceItemWithBCDEqualsStartDayWithTargetDateIn3 expectedDates.put(new LocalDate(2012, 9, 16), new LocalDate(2012, 10, 16)); expectedDates.put(new LocalDate(2012, 10, 16), new LocalDate(2012, 11, 16)); - verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); } @Test(groups = "fast") @@ -142,16 +143,15 @@ public void testCalculateSimpleInvoiceItemWithBCDAfterStartDayWithTargetDateIn3M expectedDates.put(new LocalDate(2012, 8, 17), new LocalDate(2012, 9, 17)); expectedDates.put(new LocalDate(2012, 9, 17), new LocalDate(2012, 10, 17)); - verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); } private void verifyInvoiceItems(final LocalDate startDate, final LocalDate endDate, final LocalDate targetDate, - final DateTimeZone dateTimeZone, final int billingCycleDayLocal, final BillingPeriod billingPeriod, + final int billingCycleDayLocal, final BillingPeriod billingPeriod, final LinkedHashMap expectedDates) throws InvalidDateSequenceException { - final InAdvanceBillingMode billingMode = new InAdvanceBillingMode(); - - final List invoiceItems = billingMode.generateInvoiceItemData(startDate, endDate, targetDate, billingCycleDayLocal, billingPeriod); + final RecurringInvoiceItemDataWithNextBillingCycleDate invoiceItemsWithDates = fixedAndRecurringInvoiceItemGenerator.generateInvoiceItemData(startDate, endDate, targetDate, billingCycleDayLocal, billingPeriod, BillingMode.IN_ADVANCE); + final List invoiceItems = invoiceItemsWithDates.getItemData(); int i = 0; for (final LocalDate periodStartDate : expectedDates.keySet()) { Assert.assertEquals(invoiceItems.get(i).getStartDate(), periodStartDate); diff --git a/invoice/src/test/java/org/killbill/billing/invoice/model/TestRecurringInArrear.java b/invoice/src/test/java/org/killbill/billing/invoice/model/TestRecurringInArrear.java new file mode 100644 index 0000000000..b5ee272ba0 --- /dev/null +++ b/invoice/src/test/java/org/killbill/billing/invoice/model/TestRecurringInArrear.java @@ -0,0 +1,167 @@ +/* + * 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.model; + +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.List; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.LocalDate; +import org.killbill.billing.catalog.api.BillingMode; +import org.killbill.billing.catalog.api.BillingPeriod; +import org.killbill.billing.invoice.InvoiceTestSuiteNoDB; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class TestRecurringInArrear extends InvoiceTestSuiteNoDB { + + private static final DateTimeZone TIMEZONE = DateTimeZone.forID("Pacific/Pitcairn"); + private static final BillingPeriod BILLING_PERIOD = BillingPeriod.MONTHLY; + + @Test(groups = "fast") + public void testItemShouldNotStartInThePast() throws Exception { + final LocalDate startDate = new LocalDate(2012, 7, 16); + final LocalDate endDate = new LocalDate(2012, 7, 16); + final LocalDate targetDate = new LocalDate(2012, 7, 16); + final int billingCycleDayLocal = 15; + + final LinkedHashMap expectedDates = new LinkedHashMap(); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + } + + @Test(groups = "fast") + public void testCalculateSimpleInvoiceItemWithNoEndDate() throws Exception { + final LocalDate startDate = new LocalDate(new DateTime("2012-07-17T02:25:33.000Z", DateTimeZone.UTC), TIMEZONE); + final LocalDate endDate = null; + final LocalDate targetDate = new LocalDate(2012, 7, 16); + final int billingCycleDayLocal = 15; + + final LinkedHashMap expectedDates = new LinkedHashMap(); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + } + + @Test(groups = "fast") + public void testCalculateSimpleInvoiceItemWithBCDBeforeStartDay() throws Exception { + final LocalDate startDate = new LocalDate(2012, 7, 16); + final LocalDate endDate = new LocalDate(2012, 8, 16); + final LocalDate targetDate = new LocalDate(2012, 7, 16); + final int billingCycleDayLocal = 15; + + final LinkedHashMap expectedDates = new LinkedHashMap(); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + + final LocalDate targetDate2 = new LocalDate(2012, 8, 15); + expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 15)); + verifyInvoiceItems(startDate, endDate, targetDate2, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + } + + @Test(groups = "fast") + public void testCalculateSimpleInvoiceItemWithBCDEqualsStartDay() throws Exception { + final LocalDate startDate = new LocalDate(2012, 7, 16); + final LocalDate endDate = new LocalDate(2012, 8, 16); + final LocalDate targetDate = new LocalDate(2012, 7, 16); + final int billingCycleDayLocal = 16; + + final LinkedHashMap expectedDates = new LinkedHashMap(); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + + expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 16)); + final LocalDate targetDate2 = new LocalDate(2012, 8, 16); + verifyInvoiceItems(startDate, endDate, targetDate2, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + + } + + @Test(groups = "fast") + public void testCalculateSimpleInvoiceItemWithBCDAfterStartDay() throws Exception { + final LocalDate startDate = new LocalDate(2012, 7, 16); + final LocalDate endDate = new LocalDate(2012, 8, 16); + final LocalDate targetDate = new LocalDate(2012, 7, 16); + final int billingCycleDayLocal = 17; + + final LinkedHashMap expectedDates = new LinkedHashMap(); + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + + final LocalDate targetDate2 = new LocalDate(2012, 7, 17); + expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 7, 17)); + verifyInvoiceItems(startDate, endDate, targetDate2, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + } + + @Test(groups = "fast") + public void testCalculateSimpleInvoiceItemWithBCDBeforeStartDayWithTargetDateIn3Months() throws Exception { + final LocalDate startDate = new LocalDate(2012, 7, 16); + final LocalDate endDate = null; + final LocalDate targetDate = new LocalDate(2012, 10, 16); + final int billingCycleDayLocal = 15; + + final LinkedHashMap expectedDates = new LinkedHashMap(); + expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 15)); + expectedDates.put(new LocalDate(2012, 8, 15), new LocalDate(2012, 9, 15)); + expectedDates.put(new LocalDate(2012, 9, 15), new LocalDate(2012, 10, 15)); + + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + } + + @Test(groups = "fast") + public void testCalculateSimpleInvoiceItemWithBCDEqualsStartDayWithTargetDateIn3Months() throws Exception { + final LocalDate startDate = new LocalDate(2012, 7, 16); + final LocalDate endDate = null; + final LocalDate targetDate = new LocalDate(2012, 10, 16); + final int billingCycleDayLocal = 16; + + final LinkedHashMap expectedDates = new LinkedHashMap(); + expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 16)); + expectedDates.put(new LocalDate(2012, 8, 16), new LocalDate(2012, 9, 16)); + expectedDates.put(new LocalDate(2012, 9, 16), new LocalDate(2012, 10, 16)); + + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + } + + @Test(groups = "fast") + public void testCalculateSimpleInvoiceItemWithBCDAfterStartDayWithTargetDateIn3Months() throws Exception { + final LocalDate startDate = new LocalDate(2012, 7, 16); + final LocalDate endDate = null; + final LocalDate targetDate = new LocalDate(2012, 10, 16); + final int billingCycleDayLocal = 17; + + final LinkedHashMap expectedDates = new LinkedHashMap(); + expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 7, 17)); + expectedDates.put(new LocalDate(2012, 7, 17), new LocalDate(2012, 8, 17)); + expectedDates.put(new LocalDate(2012, 8, 17), new LocalDate(2012, 9, 17)); + + verifyInvoiceItems(startDate, endDate, targetDate, billingCycleDayLocal, BILLING_PERIOD, expectedDates); + } + + private void verifyInvoiceItems(final LocalDate startDate, final LocalDate endDate, final LocalDate targetDate, + final int billingCycleDayLocal, final BillingPeriod billingPeriod, + final LinkedHashMap expectedDates) throws InvalidDateSequenceException { + + final RecurringInvoiceItemDataWithNextBillingCycleDate invoiceItemsWithDates = fixedAndRecurringInvoiceItemGenerator.generateInvoiceItemData(startDate, endDate, targetDate, billingCycleDayLocal, billingPeriod, BillingMode.IN_ARREAR); + final List invoiceItems = invoiceItemsWithDates.getItemData(); + + int i = 0; + for (final LocalDate periodStartDate : expectedDates.keySet()) { + Assert.assertEquals(invoiceItems.get(i).getStartDate(), periodStartDate); + Assert.assertEquals(invoiceItems.get(i).getEndDate(), expectedDates.get(periodStartDate)); + Assert.assertTrue(invoiceItems.get(0).getNumberOfCycles().compareTo(BigDecimal.ONE) <= 0); + i++; + } + Assert.assertEquals(invoiceItems.size(), i); + } +} diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/InternationalPriceMock.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/InternationalPriceMock.java similarity index 96% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/InternationalPriceMock.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/InternationalPriceMock.java index 803c38dafe..d604794151 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/InternationalPriceMock.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/InternationalPriceMock.java @@ -14,7 +14,7 @@ * under the License. */ -package org.killbill.billing.invoice.tests; +package org.killbill.billing.invoice.proRations; import java.math.BigDecimal; diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/InvoiceTestUtils.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java similarity index 98% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/InvoiceTestUtils.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java index 67bf3920e4..bd3103c715 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/InvoiceTestUtils.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java @@ -16,14 +16,13 @@ * under the License. */ -package org.killbill.billing.invoice.tests; +package org.killbill.billing.invoice.proRations; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.UUID; -import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.killbill.billing.account.api.Account; import org.killbill.billing.account.api.AccountApiException; @@ -135,6 +134,7 @@ public static InvoicePayment createAndPersistPayment(final InvoiceInternalApi in Mockito.when(payment.getAmount()).thenReturn(amount); Mockito.when(payment.getCurrency()).thenReturn(currency); Mockito.when(payment.getProcessedCurrency()).thenReturn(currency); + Mockito.when(payment.isSuccess()).thenReturn(true); invoicePaymentApi.notifyOfPayment(payment, callContext); diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/ProRationTestBase.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/ProRationTestBase.java similarity index 80% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/ProRationTestBase.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/ProRationTestBase.java index 4ce38af394..14c5275b21 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/ProRationTestBase.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/ProRationTestBase.java @@ -14,30 +14,31 @@ * under the License. */ -package org.killbill.billing.invoice.tests; +package org.killbill.billing.invoice.proRations; import static org.killbill.billing.invoice.TestInvoiceHelper.*; import java.math.BigDecimal; -import java.util.List; import org.joda.time.LocalDate; +import org.killbill.billing.catalog.api.BillingMode; import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.InvoiceTestSuiteNoDB; -import org.killbill.billing.invoice.model.BillingModeGenerator; import org.killbill.billing.invoice.model.InvalidDateSequenceException; import org.killbill.billing.invoice.model.RecurringInvoiceItemData; +import org.killbill.billing.invoice.model.RecurringInvoiceItemDataWithNextBillingCycleDate; import static org.testng.Assert.assertEquals; import static org.testng.Assert.fail; public abstract class ProRationTestBase extends InvoiceTestSuiteNoDB { - protected abstract BillingModeGenerator getBillingMode(); - protected abstract BillingPeriod getBillingPeriod(); + protected abstract BillingMode getBillingMode(); + + protected void testCalculateNumberOfBillingCycles(final LocalDate startDate, final LocalDate targetDate, final int billingCycleDay, final BigDecimal expectedValue) throws InvalidDateSequenceException { try { final BigDecimal numberOfBillingCycles; @@ -65,10 +66,10 @@ protected void testCalculateNumberOfBillingCycles(final LocalDate startDate, fin } protected BigDecimal calculateNumberOfBillingCycles(final LocalDate startDate, final LocalDate endDate, final LocalDate targetDate, final int billingCycleDay) throws InvalidDateSequenceException { - final List items = getBillingMode().generateInvoiceItemData(startDate, endDate, targetDate, billingCycleDay, getBillingPeriod()); + final RecurringInvoiceItemDataWithNextBillingCycleDate items = fixedAndRecurringInvoiceItemGenerator.generateInvoiceItemData(startDate, endDate, targetDate, billingCycleDay, getBillingPeriod(), getBillingMode()); BigDecimal numberOfBillingCycles = ZERO; - for (final RecurringInvoiceItemData item : items) { + for (final RecurringInvoiceItemData item : items.getItemData()) { numberOfBillingCycles = numberOfBillingCycles.add(item.getNumberOfCycles()); } @@ -76,10 +77,10 @@ protected BigDecimal calculateNumberOfBillingCycles(final LocalDate startDate, f } protected BigDecimal calculateNumberOfBillingCycles(final LocalDate startDate, final LocalDate targetDate, final int billingCycleDay) throws InvalidDateSequenceException { - final List items = getBillingMode().generateInvoiceItemData(startDate, null, targetDate, billingCycleDay, getBillingPeriod()); + final RecurringInvoiceItemDataWithNextBillingCycleDate items = fixedAndRecurringInvoiceItemGenerator.generateInvoiceItemData(startDate, null, targetDate, billingCycleDay, getBillingPeriod(), getBillingMode()); BigDecimal numberOfBillingCycles = ZERO; - for (final RecurringInvoiceItemData item : items) { + for (final RecurringInvoiceItemData item : items.getItemData()) { numberOfBillingCycles = numberOfBillingCycles.add(item.getNumberOfCycles()); } diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/GenericProRationTestBase.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/GenericProRationTestBase.java similarity index 99% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/GenericProRationTestBase.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/GenericProRationTestBase.java index 8c3774d40d..c05b8fe7f4 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/GenericProRationTestBase.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/GenericProRationTestBase.java @@ -14,7 +14,7 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance; +package org.killbill.billing.invoice.proRations.inAdvance; import static org.killbill.billing.invoice.TestInvoiceHelper.*; diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/ProRationInAdvanceTestBase.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/ProRationInAdvanceTestBase.java similarity index 68% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/ProRationInAdvanceTestBase.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/ProRationInAdvanceTestBase.java index 6fd8e15649..6ae7a219c6 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/ProRationInAdvanceTestBase.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/ProRationInAdvanceTestBase.java @@ -14,16 +14,15 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance; +package org.killbill.billing.invoice.proRations.inAdvance; -import org.killbill.billing.invoice.model.BillingModeGenerator; -import org.killbill.billing.invoice.model.InAdvanceBillingMode; -import org.killbill.billing.invoice.tests.ProRationTestBase; +import org.killbill.billing.catalog.api.BillingMode; +import org.killbill.billing.invoice.proRations.ProRationTestBase; public abstract class ProRationInAdvanceTestBase extends ProRationTestBase { @Override - protected BillingModeGenerator getBillingMode() { - return new InAdvanceBillingMode(); + protected BillingMode getBillingMode() { + return BillingMode.IN_ADVANCE; } } diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/TestValidationProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/TestValidationProRation.java similarity index 90% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/TestValidationProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/TestValidationProRation.java index 741316e494..ea1fd4bf29 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/TestValidationProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/TestValidationProRation.java @@ -14,16 +14,15 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance; +package org.killbill.billing.invoice.proRations.inAdvance; import org.joda.time.LocalDate; -import org.killbill.billing.invoice.model.BillingModeGenerator; +import org.killbill.billing.catalog.api.BillingMode; import org.testng.annotations.Test; import org.killbill.billing.catalog.api.BillingPeriod; -import org.killbill.billing.invoice.model.InAdvanceBillingMode; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.ProRationTestBase; +import org.killbill.billing.invoice.proRations.ProRationTestBase; import static org.testng.Assert.assertEquals; @@ -35,8 +34,8 @@ protected BillingPeriod getBillingPeriod() { } @Override - protected BillingModeGenerator getBillingMode() { - return new InAdvanceBillingMode(); + protected BillingMode getBillingMode() { + return BillingMode.IN_ADVANCE; } @Test(groups = "fast", expectedExceptions = InvalidDateSequenceException.class) diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/GenericProRationTests.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/GenericProRationTests.java similarity index 87% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/GenericProRationTests.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/GenericProRationTests.java index ee2cec5dc3..d6573c6e94 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/GenericProRationTests.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/GenericProRationTests.java @@ -14,14 +14,14 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.annual; +package org.killbill.billing.invoice.proRations.inAdvance.annual; import static org.killbill.billing.invoice.TestInvoiceHelper.*; import java.math.BigDecimal; import org.killbill.billing.catalog.api.BillingPeriod; -import org.killbill.billing.invoice.tests.inAdvance.GenericProRationTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.GenericProRationTestBase; public class GenericProRationTests extends GenericProRationTestBase { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestDoubleProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestDoubleProRation.java similarity index 98% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestDoubleProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestDoubleProRation.java index ffb2501c19..24c6fc89a7 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestDoubleProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestDoubleProRation.java @@ -14,7 +14,7 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.annual; +package org.killbill.billing.invoice.proRations.inAdvance.annual; import java.math.BigDecimal; @@ -23,7 +23,7 @@ import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase; import org.killbill.billing.util.currency.KillBillMoney; import static org.killbill.billing.invoice.TestInvoiceHelper.FOURTEEN; diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestLeadingProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestLeadingProRation.java similarity index 97% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestLeadingProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestLeadingProRation.java index ffed530012..341efba15c 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestLeadingProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestLeadingProRation.java @@ -14,7 +14,7 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.annual; +package org.killbill.billing.invoice.proRations.inAdvance.annual; import static org.killbill.billing.invoice.TestInvoiceHelper.*; @@ -25,7 +25,7 @@ import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase; import org.killbill.billing.util.currency.KillBillMoney; public class TestLeadingProRation extends ProRationInAdvanceTestBase { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestProRation.java similarity index 94% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestProRation.java index 49a42fe7db..e56b43fa43 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestProRation.java @@ -14,19 +14,18 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.annual; +package org.killbill.billing.invoice.proRations.inAdvance.annual; import static org.killbill.billing.invoice.TestInvoiceHelper.*; import java.math.BigDecimal; -import org.joda.time.Days; import org.joda.time.LocalDate; import org.testng.annotations.Test; import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase; import org.killbill.billing.util.currency.KillBillMoney; public class TestProRation extends ProRationInAdvanceTestBase { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestTrailingProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestTrailingProRation.java similarity index 96% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestTrailingProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestTrailingProRation.java index fe245ecf63..487710c5c4 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestTrailingProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/annual/TestTrailingProRation.java @@ -14,7 +14,7 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.annual; +package org.killbill.billing.invoice.proRations.inAdvance.annual; import static org.killbill.billing.invoice.TestInvoiceHelper.*; @@ -25,7 +25,7 @@ import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase; import org.killbill.billing.util.currency.KillBillMoney; public class TestTrailingProRation extends ProRationInAdvanceTestBase { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/GenericProRationTests.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/GenericProRationTests.java similarity index 87% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/GenericProRationTests.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/GenericProRationTests.java index 735812589b..edad4c578f 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/GenericProRationTests.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/GenericProRationTests.java @@ -14,14 +14,14 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.monthly; +package org.killbill.billing.invoice.proRations.inAdvance.monthly; import static org.killbill.billing.invoice.TestInvoiceHelper.*; import java.math.BigDecimal; import org.killbill.billing.catalog.api.BillingPeriod; -import org.killbill.billing.invoice.tests.inAdvance.GenericProRationTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.GenericProRationTestBase; public class GenericProRationTests extends GenericProRationTestBase { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestDoubleProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/TestDoubleProRation.java similarity index 97% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestDoubleProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/TestDoubleProRation.java index 64583aee33..ab66e7baa1 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestDoubleProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/TestDoubleProRation.java @@ -14,7 +14,7 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.monthly; +package org.killbill.billing.invoice.proRations.inAdvance.monthly; import static org.killbill.billing.invoice.TestInvoiceHelper.*; @@ -25,7 +25,7 @@ import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase; import org.killbill.billing.util.currency.KillBillMoney; public class TestDoubleProRation extends ProRationInAdvanceTestBase { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestLeadingProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/TestLeadingProRation.java similarity index 97% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestLeadingProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/TestLeadingProRation.java index 53ab598bf3..2b492ea5c8 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestLeadingProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/TestLeadingProRation.java @@ -14,7 +14,7 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.monthly; +package org.killbill.billing.invoice.proRations.inAdvance.monthly; import static org.killbill.billing.invoice.TestInvoiceHelper.*; @@ -25,7 +25,7 @@ import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase; import org.killbill.billing.util.currency.KillBillMoney; public class TestLeadingProRation extends ProRationInAdvanceTestBase { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/TestProRation.java similarity index 98% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/TestProRation.java index a4bd726736..87ae8027d5 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/TestProRation.java @@ -14,7 +14,7 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.monthly; +package org.killbill.billing.invoice.proRations.inAdvance.monthly; import java.math.BigDecimal; @@ -23,7 +23,7 @@ import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase; import org.killbill.billing.util.currency.KillBillMoney; import static org.killbill.billing.invoice.TestInvoiceHelper.EIGHT; diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestTrailingProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/TestTrailingProRation.java similarity index 96% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestTrailingProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/TestTrailingProRation.java index 36ccd424df..8c0f667b2c 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestTrailingProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/monthly/TestTrailingProRation.java @@ -14,7 +14,7 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.monthly; +package org.killbill.billing.invoice.proRations.inAdvance.monthly; import java.math.BigDecimal; @@ -23,7 +23,7 @@ import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase; import org.killbill.billing.util.currency.KillBillMoney; import static org.killbill.billing.invoice.TestInvoiceHelper.EIGHT; diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/GenericProRationTests.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/GenericProRationTests.java similarity index 87% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/GenericProRationTests.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/GenericProRationTests.java index 31363993bd..513f73662d 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/GenericProRationTests.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/GenericProRationTests.java @@ -14,14 +14,14 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.quarterly; +package org.killbill.billing.invoice.proRations.inAdvance.quarterly; import static org.killbill.billing.invoice.TestInvoiceHelper.*; import java.math.BigDecimal; import org.killbill.billing.catalog.api.BillingPeriod; -import org.killbill.billing.invoice.tests.inAdvance.GenericProRationTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.GenericProRationTestBase; public class GenericProRationTests extends GenericProRationTestBase { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestDoubleProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/TestDoubleProRation.java similarity index 97% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestDoubleProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/TestDoubleProRation.java index a6dd313ae9..4489758f05 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestDoubleProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/TestDoubleProRation.java @@ -14,7 +14,7 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.quarterly; +package org.killbill.billing.invoice.proRations.inAdvance.quarterly; import static org.killbill.billing.invoice.TestInvoiceHelper.*; @@ -25,7 +25,7 @@ import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase; import org.killbill.billing.util.currency.KillBillMoney; public class TestDoubleProRation extends ProRationInAdvanceTestBase { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestLeadingProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/TestLeadingProRation.java similarity index 97% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestLeadingProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/TestLeadingProRation.java index 140144f3f6..230ad69942 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestLeadingProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/TestLeadingProRation.java @@ -14,7 +14,7 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.quarterly; +package org.killbill.billing.invoice.proRations.inAdvance.quarterly; import static org.killbill.billing.invoice.TestInvoiceHelper.*; @@ -25,7 +25,7 @@ import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase; import org.killbill.billing.util.currency.KillBillMoney; public class TestLeadingProRation extends ProRationInAdvanceTestBase { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/TestProRation.java similarity index 98% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/TestProRation.java index 531573ce5f..cad222a980 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/TestProRation.java @@ -14,19 +14,18 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.quarterly; +package org.killbill.billing.invoice.proRations.inAdvance.quarterly; import static org.killbill.billing.invoice.TestInvoiceHelper.*; import java.math.BigDecimal; -import org.joda.time.Days; import org.joda.time.LocalDate; import org.testng.annotations.Test; import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase; import org.killbill.billing.util.currency.KillBillMoney; public class TestProRation extends ProRationInAdvanceTestBase { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestTrailingProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/TestTrailingProRation.java similarity index 96% rename from invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestTrailingProRation.java rename to invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/TestTrailingProRation.java index 32a47eb17a..ec2b0364d0 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestTrailingProRation.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/quarterly/TestTrailingProRation.java @@ -14,7 +14,7 @@ * under the License. */ -package org.killbill.billing.invoice.tests.inAdvance.quarterly; +package org.killbill.billing.invoice.proRations.inAdvance.quarterly; import static org.killbill.billing.invoice.TestInvoiceHelper.*; @@ -25,7 +25,7 @@ import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.invoice.model.InvalidDateSequenceException; -import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase; +import org.killbill.billing.invoice.proRations.inAdvance.ProRationInAdvanceTestBase; import org.killbill.billing.util.currency.KillBillMoney; public class TestTrailingProRation extends ProRationInAdvanceTestBase { diff --git a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java index 5e18596361..32b9b86c19 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.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 @@ -34,7 +34,6 @@ import org.killbill.billing.invoice.api.InvoiceItemType; import org.killbill.billing.invoice.api.InvoicePaymentType; import org.killbill.billing.invoice.api.formatters.InvoiceFormatter; -import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory; import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory.ResourceBundleType; import org.killbill.billing.invoice.model.CreditAdjInvoiceItem; import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem; @@ -55,12 +54,14 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB { private TranslatorConfig config; private MustacheTemplateEngine templateEngine; + private DefaultInvoiceFormatterFactory defaultInvoiceFormatterFactory; @BeforeClass(groups = "fast") public void beforeClass() throws Exception { super.beforeClass(); config = new ConfigurationObjectFactory(skifeConfigSource).build(TranslatorConfig.class); templateEngine = new MustacheTemplateEngine(); + defaultInvoiceFormatterFactory = new DefaultInvoiceFormatterFactory(); } @Test(groups = "fast") @@ -89,7 +90,7 @@ public void testIgnoreZeroAdjustments() throws Exception { Assert.assertEquals(invoice.getCreditedAmount().doubleValue(), 0.00); // Verify the merge - final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US, null, resourceBundleFactory, internalCallContext); + final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US, null, resourceBundleFactory, internalCallContext, defaultInvoiceFormatterFactory.getCurrencyLocaleMap()); final List invoiceItems = formatter.getInvoiceItems(); Assert.assertEquals(invoiceItems.size(), 1); Assert.assertEquals(invoiceItems.get(0).getInvoiceItemType(), InvoiceItemType.FIXED); @@ -134,16 +135,16 @@ public void testMergeItems() throws Exception { invoice.addInvoiceItem(creditBalanceAdjInvoiceItem2); invoice.addInvoiceItem(refundAdjInvoiceItem); invoice.addPayment(new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, UUID.randomUUID(), invoice.getId(), clock.getUTCNow(), BigDecimal.TEN, - Currency.USD, Currency.USD)); + Currency.USD, Currency.USD, true)); invoice.addPayment(new DefaultInvoicePayment(InvoicePaymentType.REFUND, UUID.randomUUID(), invoice.getId(), clock.getUTCNow(), BigDecimal.ONE.negate(), - Currency.USD, Currency.USD)); + Currency.USD, Currency.USD, true)); // Check the scenario Assert.assertEquals(invoice.getBalance().doubleValue(), 0.00); Assert.assertEquals(invoice.getCreditedAmount().doubleValue(), 11.00); Assert.assertEquals(invoice.getRefundedAmount().doubleValue(), -1.00); // Verify the merge - final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US, null, resourceBundleFactory, internalCallContext); + final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US, null, resourceBundleFactory, internalCallContext, defaultInvoiceFormatterFactory.getCurrencyLocaleMap()); final List invoiceItems = formatter.getInvoiceItems(); Assert.assertEquals(invoiceItems.size(), 4); Assert.assertEquals(invoiceItems.get(0).getInvoiceItemType(), InvoiceItemType.FIXED); @@ -157,7 +158,7 @@ public void testMergeItems() throws Exception { } @Test(groups = "fast") - public void testFormattedAmount() throws Exception { + public void testFormattedAmountFranceAndEUR() throws Exception { final FixedPriceInvoiceItem fixedItemEUR = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null, UUID.randomUUID().toString(), UUID.randomUUID().toString(), new LocalDate(), new BigDecimal("1499.95"), Currency.EUR); @@ -186,6 +187,186 @@ public void testFormattedAmount() throws Exception { Locale.FRANCE); } + @Test(groups = "fast") + public void testFormattedAmountFranceAndOMR() throws Exception { + final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null, + UUID.randomUUID().toString(), UUID.randomUUID().toString(), + new LocalDate(), new BigDecimal("1499.958"), Currency.OMR); + final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.OMR); + invoice.addInvoiceItem(fixedItem); + + checkOutput(invoice, + "\n" + + " {{invoice.formattedChargedAmount}}\n" + + "\n" + + "\n" + + " {{invoice.formattedPaidAmount}}\n" + + "\n" + + "\n" + + " {{invoice.formattedBalance}}\n" + + "", + "\n" + + " 1 499,958 ر.ع.\u200F\n" + + "\n" + + "\n" + + " 0,000 ر.ع.\u200F\n" + + "\n" + + "\n" + + " 1 499,958 ر.ع.\u200F\n" + + "", + Locale.FRANCE); + } + + @Test(groups = "fast") + public void testFormattedAmountFranceAndJPY() throws Exception { + final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null, + UUID.randomUUID().toString(), UUID.randomUUID().toString(), + new LocalDate(), new BigDecimal("1500.00"), Currency.JPY); + final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.JPY); + invoice.addInvoiceItem(fixedItem); + + checkOutput(invoice, + "\n" + + " {{invoice.formattedChargedAmount}}\n" + + "\n" + + "\n" + + " {{invoice.formattedPaidAmount}}\n" + + "\n" + + "\n" + + " {{invoice.formattedBalance}}\n" + + "", + "\n" + + " 1 500 ¥\n" + + "\n" + + "\n" + + " 0 ¥\n" + + "\n" + + "\n" + + " 1 500 ¥\n" + + "", + Locale.FRANCE); + } + + @Test(groups = "fast") + public void testFormattedAmountUSAndBTC() throws Exception { + final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null, + UUID.randomUUID().toString(), UUID.randomUUID().toString(), + new LocalDate(), new BigDecimal("1105.28843439"), Currency.BTC); + final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.BTC); + invoice.addInvoiceItem(fixedItem); + + checkOutput(invoice, + "\n" + + " {{invoice.formattedChargedAmount}}\n" + + "\n" + + "\n" + + " {{invoice.formattedPaidAmount}}\n" + + "\n" + + "\n" + + " {{invoice.formattedBalance}}\n" + + "", + "\n" + + " BTC1,105.28843439\n" + + "\n" + + "\n" + + " BTC0.00000000\n" + + "\n" + + "\n" + + " BTC1,105.28843439\n" + + "", + Locale.US); + } + + @Test(groups = "fast") + public void testFormattedAmountUSAndEUR() throws Exception { + final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null, + UUID.randomUUID().toString(), UUID.randomUUID().toString(), + new LocalDate(), new BigDecimal("2635.14"), Currency.EUR); + final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.EUR); + invoice.addInvoiceItem(fixedItem); + + checkOutput(invoice, + "\n" + + " {{invoice.formattedChargedAmount}}\n" + + "\n" + + "\n" + + " {{invoice.formattedPaidAmount}}\n" + + "\n" + + "\n" + + " {{invoice.formattedBalance}}\n" + + "", + "\n" + + " €2,635.14\n" + + "\n" + + "\n" + + " €0.00\n" + + "\n" + + "\n" + + " €2,635.14\n" + + "", + Locale.US); + } + + @Test(groups = "fast") + public void testFormattedAmountUSAndBRL() throws Exception { + final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null, + UUID.randomUUID().toString(), UUID.randomUUID().toString(), + new LocalDate(), new BigDecimal("2635.14"), Currency.BRL); + final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.BRL); + invoice.addInvoiceItem(fixedItem); + + checkOutput(invoice, + "\n" + + " {{invoice.formattedChargedAmount}}\n" + + "\n" + + "\n" + + " {{invoice.formattedPaidAmount}}\n" + + "\n" + + "\n" + + " {{invoice.formattedBalance}}\n" + + "", + "\n" + + " R$2,635.14\n" + + "\n" + + "\n" + + " R$0.00\n" + + "\n" + + "\n" + + " R$2,635.14\n" + + "", + Locale.US); + } + + @Test(groups = "fast") + public void testFormattedAmountUSAndGBP() throws Exception { + final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null, + UUID.randomUUID().toString(), UUID.randomUUID().toString(), + new LocalDate(), new BigDecimal("1499.95"), Currency.GBP); + final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.GBP); + invoice.addInvoiceItem(fixedItem); + + checkOutput(invoice, + "\n" + + " {{invoice.formattedChargedAmount}}\n" + + "\n" + + "\n" + + " {{invoice.formattedPaidAmount}}\n" + + "\n" + + "\n" + + " {{invoice.formattedBalance}}\n" + + "", + "\n" + + " £1,499.95\n" + + "\n" + + "\n" + + " £0.00\n" + + "\n" + + "\n" + + " £1,499.95\n" + + "", + Locale.US); + } + @Test(groups = "fast") public void testProcessedCurrencyExists() throws Exception { final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), clock.getUTCNow(), UUID.randomUUID(), new Integer(234), new LocalDate(), new LocalDate(), Currency.BRL, Currency.USD, false); @@ -329,7 +510,7 @@ public void testForDisplay() throws Exception { data.put("text", translator); - data.put("invoice", new DefaultInvoiceFormatter(config, invoice, Locale.US, currencyConversionApi, resourceBundleFactory, internalCallContext)); + data.put("invoice", new DefaultInvoiceFormatter(config, invoice, Locale.US, currencyConversionApi, resourceBundleFactory, internalCallContext, defaultInvoiceFormatterFactory.getCurrencyLocaleMap())); final String formattedText = templateEngine.executeTemplateText(template, data); @@ -338,7 +519,7 @@ public void testForDisplay() throws Exception { private void checkOutput(final Invoice invoice, final String template, final String expected, final Locale locale) { final Map data = new HashMap(); - data.put("invoice", new DefaultInvoiceFormatter(config, invoice, locale, null, resourceBundleFactory, internalCallContext)); + data.put("invoice", new DefaultInvoiceFormatter(config, invoice, locale, null, resourceBundleFactory, internalCallContext, defaultInvoiceFormatterFactory.getCurrencyLocaleMap())); final String formattedText = templateEngine.executeTemplateText(template, data); Assert.assertEquals(formattedText, expected); diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java index c59589f0cd..dd025c0559 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalConsumableInArrear.java @@ -35,6 +35,7 @@ import org.killbill.billing.invoice.api.InvoiceItem; import org.killbill.billing.invoice.model.FixedPriceInvoiceItem; import org.killbill.billing.invoice.model.UsageInvoiceItem; +import org.killbill.billing.invoice.usage.ContiguousIntervalConsumableInArrear.ConsumableInArrearItemsAndNextNotificationDate; import org.killbill.billing.junction.BillingEvent; import org.killbill.billing.usage.RawUsage; import org.killbill.billing.usage.api.RolledUpUsage; @@ -88,6 +89,7 @@ public void testComputeToBeBilledUsage() { final LocalDate targetDate = startDate.plusDays(1); final ContiguousIntervalConsumableInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.of(), targetDate, false, createMockBillingEvent(targetDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), + BillingPeriod.MONTHLY, Collections.emptyList()) ); @@ -127,6 +129,7 @@ public void testComputeBilledUsage() throws CatalogApiException { final ContiguousIntervalConsumableInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, ImmutableList.of(), targetDate, false, createMockBillingEvent(targetDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), + BillingPeriod.MONTHLY, Collections.emptyList()) ); @@ -156,8 +159,8 @@ public void testComputeMissingItems() throws CatalogApiException { final LocalDate targetDate = endDate; - final BillingEvent event1 = createMockBillingEvent(startDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), Collections.emptyList()); - final BillingEvent event2 = createMockBillingEvent(endDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), Collections.emptyList()); + final BillingEvent event1 = createMockBillingEvent(startDate.toDateTimeAtStartOfDay(DateTimeZone.UTC),BillingPeriod.MONTHLY, Collections.emptyList()); + final BillingEvent event2 = createMockBillingEvent(endDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.emptyList()); final ContiguousIntervalConsumableInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage, rawUsages, targetDate, true, event1, event2); @@ -168,7 +171,8 @@ public void testComputeMissingItems() throws CatalogApiException { final InvoiceItem ii2 = new UsageInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, usage.getName(), firstBCDDate, endDate, BigDecimal.ONE, currency); invoiceItems.add(ii2); - final List rawResults = intervalConsumableInArrear.computeMissingItems(invoiceItems); + final ConsumableInArrearItemsAndNextNotificationDate usageResult = intervalConsumableInArrear.computeMissingItemsAndNextNotificationDate(invoiceItems); + final List rawResults = usageResult.getInvoiceItems(); assertEquals(rawResults.size(), 4); final List result = ImmutableList.copyOf(Iterables.filter(rawResults, new Predicate() { @@ -216,16 +220,16 @@ public void testGetRolledUpUsage() { final LocalDate t0 = new LocalDate(2015, 03, BCD); - final BillingEvent eventT0 = createMockBillingEvent(t0.toDateTimeAtStartOfDay(DateTimeZone.UTC), Collections.emptyList()); + final BillingEvent eventT0 = createMockBillingEvent(t0.toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.emptyList()); final LocalDate t1 = new LocalDate(2015, 04, BCD); - final BillingEvent eventT1 = createMockBillingEvent(t1.toDateTimeAtStartOfDay(DateTimeZone.UTC), Collections.emptyList()); + final BillingEvent eventT1 = createMockBillingEvent(t1.toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.emptyList()); final LocalDate t2 = new LocalDate(2015, 05, BCD); - final BillingEvent eventT2 = createMockBillingEvent(t2.toDateTimeAtStartOfDay(DateTimeZone.UTC), Collections.emptyList()); + final BillingEvent eventT2 = createMockBillingEvent(t2.toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.emptyList()); final LocalDate t3 = new LocalDate(2015, 06, BCD); - final BillingEvent eventT3 = createMockBillingEvent(t3.toDateTimeAtStartOfDay(DateTimeZone.UTC), Collections.emptyList()); + final BillingEvent eventT3 = createMockBillingEvent(t3.toDateTimeAtStartOfDay(DateTimeZone.UTC), BillingPeriod.MONTHLY, Collections.emptyList()); final LocalDate targetDate = t3; diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestSubscriptionConsumableInArrear.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestSubscriptionConsumableInArrear.java index 7434bd8e8b..bff7839621 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestSubscriptionConsumableInArrear.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestSubscriptionConsumableInArrear.java @@ -60,27 +60,28 @@ public void testComputeInArrearUsageInterval() { final Usage usage2 = createDefaultUsage(usageName2, BillingPeriod.MONTHLY, tier2); final DateTime dt1 = new DateTime(2013, 3, 23, 4, 34, 59, DateTimeZone.UTC); - final BillingEvent evt1 = createMockBillingEvent(dt1, ImmutableList.builder().add(usage1).add(usage2).build()); + final BillingEvent evt1 = createMockBillingEvent(dt1, BillingPeriod.MONTHLY, ImmutableList.builder().add(usage1).add(usage2).build()); billingEvents.add(evt1); final DateTime dt2 = new DateTime(2013, 4, 23, 4, 34, 59, DateTimeZone.UTC); - final BillingEvent evt2 = createMockBillingEvent(dt2, ImmutableList.builder().add(usage1).build()); + final BillingEvent evt2 = createMockBillingEvent(dt2, BillingPeriod.MONTHLY, ImmutableList.builder().add(usage1).build()); billingEvents.add(evt2); final DateTime dt3 = new DateTime(2013, 5, 23, 4, 34, 59, DateTimeZone.UTC); - final BillingEvent evt3 = createMockBillingEvent(dt3, ImmutableList.builder().add(usage1).add(usage2).build()); + final BillingEvent evt3 = createMockBillingEvent(dt3, BillingPeriod.MONTHLY, ImmutableList.builder().add(usage1).add(usage2).build()); billingEvents.add(evt3); LocalDate targetDate = new LocalDate(2013, 6, 23); - final SubscriptionConsumableInArrear foo = new SubscriptionConsumableInArrear(invoiceId, billingEvents, ImmutableList.of(), targetDate, new LocalDate(dt1, DateTimeZone.UTC)); + final SubscriptionConsumableInArrear foo = new SubscriptionConsumableInArrear(accountId, invoiceId, billingEvents, ImmutableList.of(), targetDate, new LocalDate(dt1, DateTimeZone.UTC)); final List result = foo.computeInArrearUsageInterval(); assertEquals(result.size(), 3); assertEquals(result.get(0).getUsage().getName(), usageName2); - assertEquals(result.get(0).getTransitionTimes().size(), 2); + assertEquals(result.get(0).getTransitionTimes().size(), 3); assertTrue(result.get(0).getTransitionTimes().get(0).compareTo(new LocalDate(2013, 3, 23)) == 0); assertTrue(result.get(0).getTransitionTimes().get(1).compareTo(new LocalDate(2013, 4, 15)) == 0); + assertTrue(result.get(0).getTransitionTimes().get(2).compareTo(new LocalDate(2013, 4, 23)) == 0); assertEquals(result.get(1).getUsage().getName(), usageName1); assertEquals(result.get(1).getTransitionTimes().size(), 4); diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java index 6a61d18932..ac6dfdd265 100644 --- a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java +++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java @@ -71,7 +71,7 @@ protected void beforeClass() throws Exception { } protected ContiguousIntervalConsumableInArrear createContiguousIntervalConsumableInArrear(final DefaultUsage usage, List rawUsages, final LocalDate targetDate, final boolean closedInterval, final BillingEvent... events) { - final ContiguousIntervalConsumableInArrear intervalConsumableInArrear = new ContiguousIntervalConsumableInArrear(usage, invoiceId, rawUsages, targetDate, new LocalDate(events[0].getEffectiveDate())); + final ContiguousIntervalConsumableInArrear intervalConsumableInArrear = new ContiguousIntervalConsumableInArrear(usage, accountId, invoiceId, rawUsages, targetDate, new LocalDate(events[0].getEffectiveDate())); for (BillingEvent event : events) { intervalConsumableInArrear.addBillingEvent(event); } @@ -109,16 +109,17 @@ protected DefaultTieredBlock createDefaultTieredBlock(final String unit, final i return block; } - protected BillingEvent createMockBillingEvent(DateTime effectiveDate, final List usages) { + protected BillingEvent createMockBillingEvent(DateTime effectiveDate, BillingPeriod billingPeriod, final List usages) { final BillingEvent result = Mockito.mock(BillingEvent.class); Mockito.when(result.getCurrency()).thenReturn(Currency.BTC); Mockito.when(result.getBillCycleDayLocal()).thenReturn(BCD); Mockito.when(result.getTimeZone()).thenReturn(DateTimeZone.UTC); Mockito.when(result.getEffectiveDate()).thenReturn(effectiveDate); + Mockito.when(result.getBillingPeriod()).thenReturn(billingPeriod); + final Account account = Mockito.mock(Account.class); Mockito.when(account.getId()).thenReturn(accountId); - Mockito.when(result.getAccount()).thenReturn(account); final SubscriptionBase subscription = Mockito.mock(SubscriptionBase.class); Mockito.when(subscription.getId()).thenReturn(subscriptionId); diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml index 5a8de806ac..64d7eccf6e 100644 --- a/jaxrs/pom.xml +++ b/jaxrs/pom.xml @@ -21,7 +21,7 @@ killbill org.kill-bill.billing - 0.15.3-SNAPSHOT + 0.15.7-SNAPSHOT ../pom.xml killbill-jaxrs diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/DefaultJaxrsService.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/DefaultJaxrsService.java new file mode 100644 index 0000000000..bbd4d7bc95 --- /dev/null +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/DefaultJaxrsService.java @@ -0,0 +1,61 @@ +/* + * 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.jaxrs; + +import javax.inject.Inject; + +import org.killbill.billing.platform.api.LifecycleHandlerType; +import org.killbill.billing.platform.api.LifecycleHandlerType.LifecycleLevel; +import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue; +import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueAlreadyExists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DefaultJaxrsService implements JaxrsService { + + private static final Logger log = LoggerFactory.getLogger(DefaultJaxrsService.class); + + private static final String JAXRS_SERVICE_NAME = "jaxrs-service"; + + private final JaxrsExecutors jaxrsExecutors; + + @Inject + public DefaultJaxrsService(final JaxrsExecutors jaxrsExecutors) { + this.jaxrsExecutors = jaxrsExecutors; + } + + @Override + public String getName() { + return JAXRS_SERVICE_NAME; + } + + @LifecycleHandlerType(LifecycleLevel.INIT_SERVICE) + public void initialize() throws NotificationQueueAlreadyExists { + jaxrsExecutors.initialize(); + } + + @LifecycleHandlerType(LifecycleLevel.STOP_SERVICE) + public void stop() throws NoSuchNotificationQueue { + try { + jaxrsExecutors.stop(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("JaxrsService got interrupted", e); + } + } +} diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/JaxrsExecutors.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/JaxrsExecutors.java new file mode 100644 index 0000000000..b2625f343d --- /dev/null +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/JaxrsExecutors.java @@ -0,0 +1,82 @@ +/* + * 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.jaxrs; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; + +import org.killbill.billing.util.config.JaxrsConfig; +import org.killbill.commons.concurrent.WithProfilingThreadPoolExecutor; + +public class JaxrsExecutors { + + + private static final long TIMEOUT_EXECUTOR_SEC = 3L; + + private static final String JAXRS_THREAD_PREFIX = "jaxrs-th-"; + private static final String JAXRS_TH_GROUP_NAME = "jaxrs-grp"; + + + private final JaxrsConfig JaxrsConfig; + + private volatile ExecutorService jaxrsExecutorService; + + @Inject + public JaxrsExecutors(JaxrsConfig JaxrsConfig) { + this.JaxrsConfig = JaxrsConfig; + + } + + public void initialize() { + this.jaxrsExecutorService = createJaxrsExecutorService(); + } + + + public void stop() throws InterruptedException { + jaxrsExecutorService.shutdownNow(); + jaxrsExecutorService.awaitTermination(TIMEOUT_EXECUTOR_SEC, TimeUnit.SECONDS); + jaxrsExecutorService = null; + + } + + public ExecutorService getJaxrsExecutorService() { + return jaxrsExecutorService; + } + + private ExecutorService createJaxrsExecutorService() { + return new WithProfilingThreadPoolExecutor(JaxrsConfig.getJaxrsThreadNb(), + JaxrsConfig.getJaxrsThreadNb(), + 0L, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + new ThreadFactory() { + + @Override + public Thread newThread(final Runnable r) { + final Thread th = new Thread(new ThreadGroup(JAXRS_TH_GROUP_NAME), r); + th.setName(JAXRS_THREAD_PREFIX + th.getId()); + return th; + } + }); + + } +} diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/glue/DefaultJaxrsModule.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/glue/DefaultJaxrsModule.java new file mode 100644 index 0000000000..f19b31dbb7 --- /dev/null +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/glue/DefaultJaxrsModule.java @@ -0,0 +1,43 @@ +/* + * 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.jaxrs.glue; + +import org.killbill.billing.jaxrs.DefaultJaxrsService; +import org.killbill.billing.jaxrs.JaxrsExecutors; +import org.killbill.billing.jaxrs.JaxrsService; +import org.killbill.billing.platform.api.KillbillConfigSource; +import org.killbill.billing.util.config.JaxrsConfig; +import org.killbill.billing.util.glue.KillBillModule; +import org.skife.config.ConfigurationObjectFactory; + +public class DefaultJaxrsModule extends KillBillModule { + + public DefaultJaxrsModule(final KillbillConfigSource configSource) { + super(configSource); + } + + @Override + protected void configure() { + final ConfigurationObjectFactory factory = new ConfigurationObjectFactory(skifeConfigSource); + final JaxrsConfig jaxrsConfig = factory.build(JaxrsConfig.class); + bind(JaxrsConfig.class).toInstance(jaxrsConfig); + + bind(JaxrsExecutors.class).asEagerSingleton(); + bind(JaxrsService.class).to(DefaultJaxrsService.class).asEagerSingleton(); + } +} diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboHostedPaymentPageJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboHostedPaymentPageJson.java new file mode 100644 index 0000000000..8e72c5f5ff --- /dev/null +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboHostedPaymentPageJson.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015 Groupon, Inc + * Copyright 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.jaxrs.json; + +import java.util.List; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ComboHostedPaymentPageJson extends ComboPaymentJson { + + private final HostedPaymentPageFieldsJson hostedPaymentPageFields; + + @JsonCreator + public ComboHostedPaymentPageJson(@JsonProperty("account") final AccountJson account, + @JsonProperty("paymentMethod") final PaymentMethodJson paymentMethod, + @JsonProperty("hostedPaymentPageFields") final HostedPaymentPageFieldsJson hostedPaymentPageFields, + @JsonProperty("paymentMethodPluginProperties") final Iterable paymentMethodPluginProperties, + @JsonProperty("auditLogs") @Nullable final List auditLogs) { + super(account, paymentMethod, paymentMethodPluginProperties, auditLogs); + this.hostedPaymentPageFields = hostedPaymentPageFields; + } + + public HostedPaymentPageFieldsJson getHostedPaymentPageFieldsJson() { + return hostedPaymentPageFields; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ComboHostedPaymentPageJson{"); + sb.append("hostedPaymentPageFields=").append(hostedPaymentPageFields); + sb.append('}'); + return sb.toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + final ComboHostedPaymentPageJson that = (ComboHostedPaymentPageJson) o; + + return !(hostedPaymentPageFields != null ? !hostedPaymentPageFields.equals(that.hostedPaymentPageFields) : that.hostedPaymentPageFields != null); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (hostedPaymentPageFields != null ? hostedPaymentPageFields.hashCode() : 0); + return result; + } +} diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboPaymentJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboPaymentJson.java new file mode 100644 index 0000000000..be99ecaf46 --- /dev/null +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboPaymentJson.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015 Groupon, Inc + * Copyright 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.jaxrs.json; + +import java.util.List; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public abstract class ComboPaymentJson extends JsonBase { + + private final AccountJson account; + private final PaymentMethodJson paymentMethod; + private final Iterable paymentMethodPluginProperties; + + @JsonCreator + public ComboPaymentJson(@JsonProperty("account") final AccountJson account, + @JsonProperty("paymentMethod") final PaymentMethodJson paymentMethod, + @JsonProperty("paymentMethodPluginProperties") final Iterable paymentMethodPluginProperties, + @JsonProperty("auditLogs") @Nullable final List auditLogs) { + super(auditLogs); + this.account = account; + this.paymentMethod = paymentMethod; + this.paymentMethodPluginProperties = paymentMethodPluginProperties; + } + + public AccountJson getAccount() { + return account; + } + + public PaymentMethodJson getPaymentMethod() { + return paymentMethod; + } + + public Iterable getPaymentMethodPluginProperties() { + return paymentMethodPluginProperties; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ComboPaymentJson{"); + sb.append("account=").append(account); + sb.append(", paymentMethod=").append(paymentMethod); + sb.append(", paymentMethodPluginProperties=").append(paymentMethodPluginProperties); + sb.append('}'); + return sb.toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final ComboPaymentJson that = (ComboPaymentJson) o; + + if (account != null ? !account.equals(that.account) : that.account != null) { + return false; + } + if (paymentMethod != null ? !paymentMethod.equals(that.paymentMethod) : that.paymentMethod != null) { + return false; + } + return !(paymentMethodPluginProperties != null ? !paymentMethodPluginProperties.equals(that.paymentMethodPluginProperties) : that.paymentMethodPluginProperties != null); + } + + @Override + public int hashCode() { + int result = account != null ? account.hashCode() : 0; + result = 31 * result + (paymentMethod != null ? paymentMethod.hashCode() : 0); + result = 31 * result + (paymentMethodPluginProperties != null ? paymentMethodPluginProperties.hashCode() : 0); + return result; + } +} diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboPaymentTransactionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboPaymentTransactionJson.java index 9a5074167f..3ca1eadfd8 100644 --- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboPaymentTransactionJson.java +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboPaymentTransactionJson.java @@ -1,6 +1,6 @@ /* - * Copyright 2014-2015 Groupon, Inc - * Copyright 2014-2015 The Billing Project, LLC + * Copyright 2015 Groupon, Inc + * Copyright 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 @@ -24,12 +24,9 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -public class ComboPaymentTransactionJson extends JsonBase { +public class ComboPaymentTransactionJson extends ComboPaymentJson { - private final AccountJson account; - private final PaymentMethodJson paymentMethod; private final PaymentTransactionJson transaction; - private final Iterable paymentMethodPluginProperties; private final Iterable transactionPluginProperties; @JsonCreator @@ -39,44 +36,26 @@ public ComboPaymentTransactionJson(@JsonProperty("account") final AccountJson ac @JsonProperty("paymentMethodPluginProperties") final Iterable paymentMethodPluginProperties, @JsonProperty("transactionPluginProperties") final Iterable transactionPluginProperties, @JsonProperty("auditLogs") @Nullable final List auditLogs) { - super(auditLogs); - this.account = account; - this.paymentMethod = paymentMethod; + super(account, paymentMethod, paymentMethodPluginProperties, auditLogs); this.transaction = transaction; - this.paymentMethodPluginProperties = paymentMethodPluginProperties; this.transactionPluginProperties = transactionPluginProperties; } - - public AccountJson getAccount() { - return account; - } - - public PaymentMethodJson getPaymentMethod() { - return paymentMethod; - } - public PaymentTransactionJson getTransaction() { return transaction; } - public Iterable getPaymentMethodPluginProperties() { - return paymentMethodPluginProperties; - } - public Iterable getTransactionPluginProperties() { return transactionPluginProperties; } @Override public String toString() { - return "ComboPaymentTransactionJson{" + - "account=" + account + - ", paymentMethod=" + paymentMethod + - ", transaction=" + transaction + - ", paymentMethodPluginProperties=" + paymentMethodPluginProperties + - ", transactionPluginProperties=" + transactionPluginProperties + - '}'; + final StringBuilder sb = new StringBuilder("ComboPaymentTransactionJson{"); + sb.append("transaction=").append(transaction); + sb.append(", transactionPluginProperties=").append(transactionPluginProperties); + sb.append('}'); + return sb.toString(); } @Override @@ -84,34 +63,25 @@ public boolean equals(final Object o) { if (this == o) { return true; } - if (!(o instanceof ComboPaymentTransactionJson)) { + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { return false; } final ComboPaymentTransactionJson that = (ComboPaymentTransactionJson) o; - if (account != null ? !account.equals(that.account) : that.account != null) { - return false; - } - if (paymentMethod != null ? !paymentMethod.equals(that.paymentMethod) : that.paymentMethod != null) { - return false; - } if (transaction != null ? !transaction.equals(that.transaction) : that.transaction != null) { return false; } - if (paymentMethodPluginProperties != null ? !paymentMethodPluginProperties.equals(that.paymentMethodPluginProperties) : that.paymentMethodPluginProperties != null) { - return false; - } return !(transactionPluginProperties != null ? !transactionPluginProperties.equals(that.transactionPluginProperties) : that.transactionPluginProperties != null); - } @Override public int hashCode() { - int result = account != null ? account.hashCode() : 0; - result = 31 * result + (paymentMethod != null ? paymentMethod.hashCode() : 0); + int result = super.hashCode(); result = 31 * result + (transaction != null ? transaction.hashCode() : 0); - result = 31 * result + (paymentMethodPluginProperties != null ? paymentMethodPluginProperties.hashCode() : 0); result = 31 * result + (transactionPluginProperties != null ? transactionPluginProperties.hashCode() : 0); return result; } diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceDryRunJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceDryRunJson.java index 9bc33acad6..bc82ab30b8 100644 --- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceDryRunJson.java +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceDryRunJson.java @@ -28,6 +28,7 @@ public class InvoiceDryRunJson { + private final String dryRunType; private final String dryRunAction; private final String phaseType; private final String productName; @@ -41,7 +42,8 @@ public class InvoiceDryRunJson { private final List priceOverrides; @JsonCreator - public InvoiceDryRunJson(@JsonProperty("dryRunAction") @Nullable final String dryRunAction, + public InvoiceDryRunJson(@JsonProperty("dryRunType") @Nullable final String dryRunType, + @JsonProperty("dryRunAction") @Nullable final String dryRunAction, @JsonProperty("phaseType") @Nullable final String phaseType, @JsonProperty("productName") @Nullable final String productName, @JsonProperty("productCategory") @Nullable final String productCategory, @@ -52,6 +54,7 @@ public InvoiceDryRunJson(@JsonProperty("dryRunAction") @Nullable final String dr @JsonProperty("effectiveDate") @Nullable final LocalDate effectiveDate, @JsonProperty("billingPolicy") @Nullable final String billingPolicy, @JsonProperty("priceOverrides") @Nullable final List priceOverrides) { + this.dryRunType = dryRunType; this.dryRunAction = dryRunAction; this.phaseType = phaseType; this.productName = productName; @@ -65,6 +68,10 @@ public InvoiceDryRunJson(@JsonProperty("dryRunAction") @Nullable final String dr this.priceOverrides = priceOverrides; } + public String getDryRunType() { + return dryRunType; + } + public String getDryRunAction() { return dryRunAction; } @@ -129,6 +136,9 @@ public boolean equals(final Object o) { if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) { return false; } + if (dryRunType != null ? !dryRunType.equals(that.dryRunType) : that.dryRunType != null) { + return false; + } if (dryRunAction != null ? !dryRunAction.equals(that.dryRunAction) : that.dryRunAction != null) { return false; } @@ -160,6 +170,7 @@ public boolean equals(final Object o) { @Override public int hashCode() { int result = dryRunAction != null ? dryRunAction.hashCode() : 0; + result = 31 * result + (dryRunType != null ? dryRunType.hashCode() : 0); result = 31 * result + (phaseType != null ? phaseType.hashCode() : 0); result = 31 * result + (productName != null ? productName.hashCode() : 0); result = 31 * result + (productCategory != null ? productCategory.hashCode() : 0); diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TagJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TagJson.java index 0e8233d772..16dc757a36 100644 --- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TagJson.java +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TagJson.java @@ -36,24 +36,28 @@ public class TagJson extends JsonBase { @ApiModelProperty(dataType = "org.killbill.billing.ObjectType") private final ObjectType objectType; @ApiModelProperty(dataType = "java.util.UUID") + private final String objectId; + @ApiModelProperty(dataType = "java.util.UUID") private final String tagDefinitionId; private final String tagDefinitionName; @JsonCreator public TagJson(@JsonProperty("tagId") final String tagId, @JsonProperty("objectType") final ObjectType objectType, + @JsonProperty("objectId") final String objectId, @JsonProperty("tagDefinitionId") final String tagDefinitionId, @JsonProperty("tagDefinitionName") final String tagDefinitionName, @JsonProperty("auditLogs") @Nullable final List auditLogs) { super(auditLogs); this.tagId = tagId; this.objectType = objectType; + this.objectId = objectId; this.tagDefinitionId = tagDefinitionId; this.tagDefinitionName = tagDefinitionName; } public TagJson(final Tag tag, final TagDefinition tagDefinition, @Nullable final List auditLogs) { - this(tag.getId().toString(), tag.getObjectType(), tagDefinition.getId().toString(), tagDefinition.getName(), toAuditLogJson(auditLogs)); + this(tag.getId().toString(), tag.getObjectType(), tag.getObjectId().toString(), tagDefinition.getId().toString(), tagDefinition.getName(), toAuditLogJson(auditLogs)); } public String getTagId() { @@ -72,11 +76,16 @@ public String getTagDefinitionName() { return tagDefinitionName; } + public String getObjectId() { + return objectId; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder("TagJson{"); sb.append("tagId='").append(tagId).append('\''); sb.append(", objectType=").append(objectType); + sb.append(", objectId=").append(objectId); sb.append(", tagDefinitionId='").append(tagDefinitionId).append('\''); sb.append(", tagDefinitionName='").append(tagDefinitionName).append('\''); sb.append('}'); @@ -100,6 +109,9 @@ public boolean equals(final Object o) { if (tagDefinitionId != null ? !tagDefinitionId.equals(tagJson.tagDefinitionId) : tagJson.tagDefinitionId != null) { return false; } + if (objectId != null ? !objectId.equals(tagJson.objectId) : tagJson.objectId != null) { + return false; + } if (tagDefinitionName != null ? !tagDefinitionName.equals(tagJson.tagDefinitionName) : tagJson.tagDefinitionName != null) { return false; } @@ -115,6 +127,7 @@ public int hashCode() { int result = tagId != null ? tagId.hashCode() : 0; result = 31 * result + (objectType != null ? objectType.hashCode() : 0); result = 31 * result + (tagDefinitionId != null ? tagDefinitionId.hashCode() : 0); + result = 31 * result + (objectId != null ? objectId.hashCode() : 0); result = 31 * result + (tagDefinitionName != null ? tagDefinitionName.hashCode() : 0); return result; } diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionRepairExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionRepairExceptionMapper.java index 68f99d762a..1edf4e0bc3 100644 --- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionRepairExceptionMapper.java +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionRepairExceptionMapper.java @@ -38,35 +38,7 @@ public SubscriptionRepairExceptionMapper(@Context final UriInfo uriInfo) { @Override public Response toResponse(final SubscriptionBaseRepairException exception) { - if (exception.getCode() == ErrorCode.SUB_REPAIR_AO_CREATE_BEFORE_BP_START.getCode()) { - return buildBadRequestResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_BP_RECREATE_MISSING_AO.getCode()) { - return buildBadRequestResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_BP_RECREATE_MISSING_AO_CREATE.getCode()) { - return buildBadRequestResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_INVALID_DELETE_SET.getCode()) { - return buildBadRequestResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_MISSING_AO_DELETE_EVENT.getCode()) { - return buildBadRequestResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_NEW_EVENT_BEFORE_LAST_AO_REMAINING.getCode()) { - return buildBadRequestResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_NEW_EVENT_BEFORE_LAST_BP_REMAINING.getCode()) { - return buildBadRequestResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_NO_ACTIVE_SUBSCRIPTIONS.getCode()) { - return buildBadRequestResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_NON_EXISTENT_DELETE_EVENT.getCode()) { - return buildNotFoundResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_SUB_EMPTY.getCode()) { - return buildBadRequestResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_SUB_RECREATE_NOT_EMPTY.getCode()) { - return buildBadRequestResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_UNKNOWN_BUNDLE.getCode()) { - return buildNotFoundResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_UNKNOWN_SUBSCRIPTION.getCode()) { - return buildNotFoundResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_UNKNOWN_TYPE.getCode()) { - return buildNotFoundResponse(exception, uriInfo); - } else if (exception.getCode() == ErrorCode.SUB_REPAIR_VIEW_CHANGED.getCode()) { + if (exception.getCode() == ErrorCode.SUB_NO_ACTIVE_SUBSCRIPTIONS.getCode()) { return buildBadRequestResponse(exception, uriInfo); } else { return fallback(exception, uriInfo); diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java index bb57cb7b0d..0bd150295d 100644 --- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java @@ -26,6 +26,12 @@ import java.util.LinkedList; import java.util.List; import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; @@ -43,12 +49,14 @@ import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; +import org.joda.time.DateTimeZone; 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.AccountUserApi; import org.killbill.billing.account.api.MutableAccountData; import org.killbill.billing.catalog.api.Currency; @@ -60,6 +68,7 @@ import org.killbill.billing.invoice.api.InvoicePayment; import org.killbill.billing.invoice.api.InvoicePaymentApi; import org.killbill.billing.invoice.api.InvoiceUserApi; +import org.killbill.billing.jaxrs.JaxrsExecutors; import org.killbill.billing.jaxrs.json.AccountEmailJson; import org.killbill.billing.jaxrs.json.AccountJson; import org.killbill.billing.jaxrs.json.AccountTimelineJson; @@ -97,9 +106,11 @@ import org.killbill.billing.util.audit.AccountAuditLogs; import org.killbill.billing.util.callcontext.CallContext; import org.killbill.billing.util.callcontext.TenantContext; +import org.killbill.billing.util.config.JaxrsConfig; import org.killbill.billing.util.config.PaymentConfig; import org.killbill.billing.util.entity.Pagination; import org.killbill.billing.util.tag.ControlTagType; +import org.killbill.billing.util.tag.Tag; import org.killbill.clock.Clock; import com.codahale.metrics.annotation.Timed; @@ -130,6 +141,8 @@ public class AccountResource extends JaxRsResourceBase { private final InvoicePaymentApi invoicePaymentApi; private final OverdueInternalApi overdueApi; private final PaymentConfig paymentConfig; + private final JaxrsExecutors jaxrsExecutors; + private final JaxrsConfig jaxrsConfig; @Inject public AccountResource(final JaxrsUriBuilder uriBuilder, @@ -141,9 +154,12 @@ public AccountResource(final JaxrsUriBuilder uriBuilder, final AuditUserApi auditUserApi, final CustomFieldUserApi customFieldUserApi, final SubscriptionApi subscriptionApi, + final AccountInternalApi accountInternalApi, final OverdueInternalApi overdueApi, final Clock clock, final PaymentConfig paymentConfig, + final JaxrsExecutors jaxrsExecutors, + final JaxrsConfig jaxrsConfig, final Context context) { super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountApi, paymentApi, clock, context); this.subscriptionApi = subscriptionApi; @@ -151,6 +167,8 @@ public AccountResource(final JaxrsUriBuilder uriBuilder, this.invoicePaymentApi = invoicePaymentApi; this.overdueApi = overdueApi; this.paymentConfig = paymentConfig; + this.jaxrsExecutors = jaxrsExecutors; + this.jaxrsConfig = jaxrsConfig; } @Timed @@ -352,6 +370,7 @@ public Response cancelAccount(@PathParam("accountId") final String accountId, return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } + @Timed @GET @Path("/{accountId:" + UUID_PATTERN + "}/" + TIMELINE) @@ -361,28 +380,127 @@ public Response cancelAccount(@PathParam("accountId") final String accountId, @ApiResponse(code = 404, message = "Account not found")}) public Response getAccountTimeline(@PathParam("accountId") final String accountIdString, @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode, - @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, PaymentApiException, SubscriptionApiException { + @QueryParam(QUERY_PARALLEL) @DefaultValue("false") final Boolean parallel, + @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, PaymentApiException, SubscriptionApiException, InvoiceApiException { final TenantContext tenantContext = context.createContext(request); final UUID accountId = UUID.fromString(accountIdString); final Account account = accountUserApi.getAccountById(accountId, tenantContext); - // Get the invoices - final List invoices = invoiceApi.getInvoicesByAccount(account.getId(), tenantContext); + final Callable> bundlesCallable = new Callable>() { + @Override + public List call() throws Exception { + return subscriptionApi.getSubscriptionBundlesForAccountId(account.getId(), tenantContext); + } + }; + final Callable> invoicesCallable = new Callable>() { + @Override + public List call() throws Exception { + return invoiceApi.getInvoicesByAccount(account.getId(), tenantContext); + } + }; + final Callable> invoicePaymentsCallable = new Callable>() { + @Override + public List call() throws Exception { + return invoicePaymentApi.getInvoicePaymentsByAccount(accountId, tenantContext); + } + }; + final Callable> paymentsCallable = new Callable>() { + @Override + public List call() throws Exception { + return paymentApi.getAccountPayments(accountId, false, ImmutableList.of(), tenantContext); + } + }; + final Callable auditsCallable = new Callable() { + @Override + public AccountAuditLogs call() throws Exception { + return auditUserApi.getAccountAuditLogs(accountId, auditMode.getLevel(), tenantContext); + } + }; + + final AccountTimelineJson json; + + List invoices = null; + List bundles = null; + List invoicePayments = null; + List payments = null; + AccountAuditLogs accountAuditLogs = null; + + if (parallel) { + + final ExecutorService executor = jaxrsExecutors.getJaxrsExecutorService(); + final Future> futureBundlesCallable = executor.submit(bundlesCallable); + final Future> futureInvoicesCallable = executor.submit(invoicesCallable); + final Future> futureInvoicePaymentsCallable = executor.submit(invoicePaymentsCallable); + final Future> futurePaymentsCallable = executor.submit(paymentsCallable); + final Future futureAuditsCallable = executor.submit(auditsCallable); + + try { + long ini = System.currentTimeMillis(); + do { + bundles = (bundles == null) ? runCallableAndHandleTimeout(futureBundlesCallable, 100) : bundles; + invoices = (invoices == null) ? runCallableAndHandleTimeout(futureInvoicesCallable, 100) : invoices; + invoicePayments = (invoicePayments == null) ? runCallableAndHandleTimeout(futureInvoicePaymentsCallable, 100) : invoicePayments; + payments = (payments == null) ? runCallableAndHandleTimeout(futurePaymentsCallable, 100) : payments; + accountAuditLogs = (accountAuditLogs == null) ? runCallableAndHandleTimeout(futureAuditsCallable, 100) : accountAuditLogs; + } while ((System.currentTimeMillis() - ini < jaxrsConfig.getJaxrsTimeout().getMillis()) && + (bundles == null || invoices == null || invoicePayments == null || payments == null || accountAuditLogs == null)); + + if (bundles == null || invoices == null || invoicePayments == null || payments == null || accountAuditLogs == null) { + Response.status(Status.SERVICE_UNAVAILABLE).build(); + } + } catch (InterruptedException e) { + handleCallableException(e, ImmutableList.of(futureBundlesCallable, futureInvoicesCallable, futureInvoicePaymentsCallable, futurePaymentsCallable, futureAuditsCallable)); + } catch (ExecutionException e) { + handleCallableException(e.getCause(), ImmutableList.of(futureBundlesCallable, futureInvoicesCallable, futureInvoicePaymentsCallable, futurePaymentsCallable, futureAuditsCallable)); + } + + } else { + try { + invoices = invoicesCallable.call(); + payments = paymentsCallable.call(); + bundles = bundlesCallable.call(); + accountAuditLogs = auditsCallable.call(); + invoicePayments = invoicePaymentsCallable.call(); + } catch (Exception e) { + handleCallableException(e); + } + } - // Get the payments - final List payments = paymentApi.getAccountPayments(accountId, false, ImmutableList.of(), tenantContext); + json = new AccountTimelineJson(account, invoices, payments, invoicePayments, bundles, accountAuditLogs); + return Response.status(Status.OK).entity(json).build(); + } - // Get the bundles - final List bundles = subscriptionApi.getSubscriptionBundlesForAccountId(account.getId(), tenantContext); + private T runCallableAndHandleTimeout(final Future future, final long timeoutMsec) throws ExecutionException, InterruptedException { + try { + return future.get(timeoutMsec, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + return null; + } + } - // Get all audit logs - final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(accountId, auditMode.getLevel(), tenantContext); + private void handleCallableException(final Throwable causeOrException, final List toBeCancelled) throws AccountApiException, SubscriptionApiException, PaymentApiException, InvoiceApiException { + for (final Future f : toBeCancelled) { + f.cancel(true); + } + handleCallableException(causeOrException); + } - final List invoicePayments = invoicePaymentApi.getInvoicePaymentsByAccount(accountId, tenantContext); - final AccountTimelineJson json = new AccountTimelineJson(account, invoices, payments, invoicePayments, bundles, - accountAuditLogs); - return Response.status(Status.OK).entity(json).build(); + private void handleCallableException(final Throwable causeOrException) throws AccountApiException, SubscriptionApiException, PaymentApiException, InvoiceApiException { + if (causeOrException instanceof AccountApiException) { + throw (AccountApiException) causeOrException; + } else if (causeOrException instanceof SubscriptionApiException) { + throw (SubscriptionApiException) causeOrException; + } else if (causeOrException instanceof InvoiceApiException) { + throw (InvoiceApiException) causeOrException; + } else if (causeOrException instanceof PaymentApiException) { + throw (PaymentApiException) causeOrException; + } else { + if (causeOrException instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new RuntimeException(causeOrException.getMessage(), causeOrException); + } } /* @@ -558,7 +676,8 @@ public Response payAllInvoices(@PathParam("accountId") final String accountId, final BigDecimal amountToPay = (remainingRequestPayment.compareTo(invoice.getBalance()) >= 0) ? invoice.getBalance() : remainingRequestPayment; if (amountToPay.compareTo(BigDecimal.ZERO) > 0) { - createPurchaseForInvoice(account, invoice.getId(), amountToPay, externalPayment, pluginProperties, callContext); + final UUID paymentMethodId = externalPayment ? null : account.getPaymentMethodId(); + createPurchaseForInvoice(account, invoice.getId(), amountToPay, paymentMethodId, externalPayment, pluginProperties, callContext); } remainingRequestPayment = remainingRequestPayment.subtract(amountToPay); if (remainingRequestPayment.compareTo(BigDecimal.ZERO) == 0) { @@ -612,7 +731,7 @@ public Response createPaymentMethod(final PaymentMethodJson json, final UUID paymentMethodId = paymentApi.addPaymentMethod(account, data.getExternalKey(), data.getPluginName(), isDefault, data.getPluginDetail(), pluginProperties, callContext); if (payAllUnpaidInvoices && unpaidInvoices.size() > 0) { for (final Invoice invoice : unpaidInvoices) { - createPurchaseForInvoice(account, invoice.getId(), invoice.getBalance(), false, pluginProperties, callContext); + createPurchaseForInvoice(account, invoice.getId(), invoice.getBalance(), paymentMethodId, false, pluginProperties, callContext); } } return uriBuilder.buildResponse(PaymentMethodResource.class, "getPaymentMethod", paymentMethodId, uriInfo.getBaseUri().toString()); @@ -671,7 +790,7 @@ public Response setDefaultPaymentMethod(@PathParam("accountId") final String acc if (payAllUnpaidInvoices) { final Collection unpaidInvoices = invoiceApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext); for (final Invoice invoice : unpaidInvoices) { - createPurchaseForInvoice(account, invoice.getId(), invoice.getBalance(), false, pluginProperties, callContext); + createPurchaseForInvoice(account, invoice.getId(), invoice.getBalance(), account.getPaymentMethodId(), false, pluginProperties, callContext); } } return Response.status(Status.OK).build(); @@ -705,6 +824,30 @@ public PaymentJson apply(final Payment payment) { return Response.status(Response.Status.OK).entity(result).build(); } + @Timed + @POST + @Path("/" + PAYMENTS) + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @ApiOperation(value = "Trigger a payment using the account external key (authorization, purchase or credit)") + @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account external key supplied"), + @ApiResponse(code = 404, message = "Account not found")}) + public Response processPaymentByExternalKey(final PaymentTransactionJson json, + @QueryParam(QUERY_EXTERNAL_KEY) final String externalKey, + @QueryParam(QUERY_PAYMENT_METHOD_ID) final String paymentMethodIdStr, + @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List paymentControlPluginNames, + @QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString, + @HeaderParam(HDR_CREATED_BY) final String createdBy, + @HeaderParam(HDR_REASON) final String reason, + @HeaderParam(HDR_COMMENT) final String comment, + @javax.ws.rs.core.Context final UriInfo uriInfo, + @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException { + final CallContext callContext = context.createContext(createdBy, reason, comment, request); + final Account account = accountUserApi.getAccountByKey(externalKey, callContext); + + return processPayment(json, account, paymentMethodIdStr, paymentControlPluginNames, pluginPropertiesString, uriInfo, callContext); + } + @Timed @POST @Path("/{accountId:" + UUID_PATTERN + "}/" + PAYMENTS) @@ -723,19 +866,30 @@ public Response processPayment(final PaymentTransactionJson json, @HeaderParam(HDR_COMMENT) final String comment, @javax.ws.rs.core.Context final UriInfo uriInfo, @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException { + final UUID accountId = UUID.fromString(accountIdStr); + final CallContext callContext = context.createContext(createdBy, reason, comment, request); + final Account account = accountUserApi.getAccountById(accountId, callContext); + + return processPayment(json, account, paymentMethodIdStr, paymentControlPluginNames, pluginPropertiesString, uriInfo, callContext); + } + + private Response processPayment(final PaymentTransactionJson json, + final Account account, + final String paymentMethodIdStr, + final List paymentControlPluginNames, + final List pluginPropertiesString, + final UriInfo uriInfo, + final CallContext callContext) throws PaymentApiException { verifyNonNullOrEmpty(json, "PaymentTransactionJson body should be specified"); verifyNonNullOrEmpty(json.getTransactionType(), "PaymentTransactionJson transactionType needs to be set", json.getAmount(), "PaymentTransactionJson amount needs to be set"); final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString); - final CallContext callContext = context.createContext(createdBy, reason, comment, request); - final UUID accountId = UUID.fromString(accountIdStr); - final Account account = accountUserApi.getAccountById(accountId, callContext); final UUID paymentMethodId = paymentMethodIdStr == null ? account.getPaymentMethodId() : UUID.fromString(paymentMethodIdStr); final Currency currency = json.getCurrency() == null ? account.getCurrency() : Currency.valueOf(json.getCurrency()); final UUID paymentId = json.getPaymentId() == null ? null : UUID.fromString(json.getPaymentId()); - validatePaymentMethodForAccount(accountId, paymentMethodId, callContext); + validatePaymentMethodForAccount(account.getId(), paymentMethodId, callContext); final TransactionType transactionType = TransactionType.valueOf(json.getTransactionType()); final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames); @@ -856,6 +1010,27 @@ public Response getTags(@PathParam(ID_PARAM_NAME) final String accountIdString, return super.getTags(accountId, accountId, auditMode, includedDeleted, context.createContext(request)); } + @Timed + @GET + @Path("/{accountId:" + UUID_PATTERN + "}/" + ALL_TAGS) + @Produces(APPLICATION_JSON) + @ApiOperation(value = "Retrieve account tags", response = TagJson.class, responseContainer = "List") + @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"), + @ApiResponse(code = 404, message = "Account not found")}) + public Response getAllTags(@PathParam(ID_PARAM_NAME) final String accountIdString, + @QueryParam(QUERY_OBJECT_TYPE) final ObjectType objectType, + @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode, + @QueryParam(QUERY_TAGS_INCLUDED_DELETED) @DefaultValue("false") final Boolean includedDeleted, + @javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException { + final UUID accountId = UUID.fromString(accountIdString); + final TenantContext tenantContext = context.createContext(request); + final List tags = objectType != null ? + tagUserApi.getTagsForAccountType(accountId, objectType, includedDeleted, tenantContext) : + tagUserApi.getTagsForAccount(accountId, includedDeleted, tenantContext); + return createTagResponse(accountId, tags, auditMode, tenantContext); + } + + @Timed @POST @Path("/{accountId:" + UUID_PATTERN + "}/" + TAGS) diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/ComboPaymentResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/ComboPaymentResource.java new file mode 100644 index 0000000000..e30864195e --- /dev/null +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/ComboPaymentResource.java @@ -0,0 +1,129 @@ +/* + * Copyright 2015 Groupon, Inc + * Copyright 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.jaxrs.resources; + +import java.util.List; +import java.util.UUID; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import org.killbill.billing.ErrorCode; +import org.killbill.billing.account.api.Account; +import org.killbill.billing.account.api.AccountApiException; +import org.killbill.billing.account.api.AccountUserApi; +import org.killbill.billing.jaxrs.json.AccountJson; +import org.killbill.billing.jaxrs.json.PaymentMethodJson; +import org.killbill.billing.jaxrs.util.Context; +import org.killbill.billing.jaxrs.util.JaxrsUriBuilder; +import org.killbill.billing.payment.api.Payment; +import org.killbill.billing.payment.api.PaymentApi; +import org.killbill.billing.payment.api.PaymentApiException; +import org.killbill.billing.payment.api.PaymentMethod; +import org.killbill.billing.payment.api.PluginProperty; +import org.killbill.billing.util.api.AuditUserApi; +import org.killbill.billing.util.api.CustomFieldUserApi; +import org.killbill.billing.util.api.TagUserApi; +import org.killbill.billing.util.callcontext.CallContext; +import org.killbill.billing.util.callcontext.TenantContext; +import org.killbill.clock.Clock; + +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +public abstract class ComboPaymentResource extends JaxRsResourceBase { + + @Inject + public ComboPaymentResource(final JaxrsUriBuilder uriBuilder, + final TagUserApi tagUserApi, + final CustomFieldUserApi customFieldUserApi, + final AuditUserApi auditUserApi, + final AccountUserApi accountUserApi, + final PaymentApi paymentApi, + final Clock clock, + final Context context) { + super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, paymentApi, clock, context); + } + + protected Account getOrCreateAccount(final AccountJson accountJson, final CallContext callContext) throws AccountApiException { + // Attempt to retrieve by accountId if specified + if (accountJson.getAccountId() != null) { + return accountUserApi.getAccountById(UUID.fromString(accountJson.getAccountId()), callContext); + } + + if (accountJson.getExternalKey() != null) { + // Attempt to retrieve by account externalKey, ignore if does not exist so we can create it with the key specified. + try { + return accountUserApi.getAccountByKey(accountJson.getExternalKey(), callContext); + } catch (final AccountApiException ignore) { + } + } + // Finally create if does not exist + return accountUserApi.createAccount(accountJson.toAccountData(), callContext); + } + + protected UUID getOrCreatePaymentMethod(final Account account, final PaymentMethodJson paymentMethodJson, final Iterable pluginProperties, final CallContext callContext) throws PaymentApiException { + // Get all payment methods for account + final List accountPaymentMethods = paymentApi.getAccountPaymentMethods(account.getId(), false, ImmutableList.of(), callContext); + + // If we were specified a paymentMethod id and we find it, we return it + if (paymentMethodJson.getPaymentMethodId() != null) { + final UUID match = UUID.fromString(paymentMethodJson.getPaymentMethodId()); + if (Iterables.any(accountPaymentMethods, new Predicate() { + @Override + public boolean apply(final PaymentMethod input) { + return input.getId().equals(match); + } + })) { + return match; + } + throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD, match); + } + + // If we were specified a paymentMethod externalKey and we find it, we return it + if (paymentMethodJson.getExternalKey() != null) { + final PaymentMethod match = Iterables.tryFind(accountPaymentMethods, new Predicate() { + @Override + public boolean apply(final PaymentMethod input) { + return input.getExternalKey().equals(paymentMethodJson.getExternalKey()); + } + }).orNull(); + if (match != null) { + return match.getId(); + } + } + + // Only set as default if this is the first paymentMethod on the account + final boolean isDefault = accountPaymentMethods.isEmpty(); + final PaymentMethod paymentData = paymentMethodJson.toPaymentMethod(account.getId().toString()); + return paymentApi.addPaymentMethod(account, paymentMethodJson.getExternalKey(), paymentMethodJson.getPluginName(), isDefault, + paymentData.getPluginDetail(), pluginProperties, callContext); + } + + protected Payment getPaymentByIdOrKey(@Nullable final String paymentIdStr, @Nullable final String externalKey, final Iterable pluginProperties, final TenantContext tenantContext) throws PaymentApiException { + Preconditions.checkArgument(paymentIdStr != null || externalKey != null, "Need to set either paymentId or payment externalKey"); + if (paymentIdStr != null) { + final UUID paymentId = UUID.fromString(paymentIdStr); + return paymentApi.getPayment(paymentId, false, pluginProperties, tenantContext); + } else { + return paymentApi.getPaymentByExternalKey(externalKey, false, pluginProperties, tenantContext); + } + } +} diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java index d45a84b631..3994d2bc48 100644 --- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java @@ -24,6 +24,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -69,6 +70,7 @@ import org.killbill.billing.entitlement.api.SubscriptionApiException; 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.InvoiceItem; @@ -109,10 +111,13 @@ import com.codahale.metrics.annotation.Timed; import com.google.common.base.Function; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; import com.google.inject.Inject; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; @@ -136,6 +141,13 @@ public class InvoiceResource extends JaxRsResourceBase { private final TenantUserApi tenantApi; private final Locale defaultLocale; + private static final Ordering INVOICE_PAYMENT_ORDERING = Ordering.from(new Comparator() { + @Override + public int compare(final InvoicePaymentJson o1, final InvoicePaymentJson o2) { + return o1.getTransactions().get(0).getEffectiveDate().compareTo(o2.getTransactions().get(0).getEffectiveDate()); + } + }); + @Inject public InvoiceResource(final AccountUserApi accountUserApi, final InvoiceUserApi invoiceApi, @@ -304,7 +316,6 @@ public Response createFutureInvoice(@QueryParam(QUERY_ACCOUNT_ID) final String a } } - @Timed @POST @Path("/" + DRY_RUN) @@ -322,14 +333,14 @@ public Response generateDryRunInvoice(@Nullable final InvoiceDryRunJson dryRunSu @javax.ws.rs.core.Context final UriInfo uriInfo) throws AccountApiException, InvoiceApiException { final CallContext callContext = context.createContext(createdBy, reason, comment, request); final LocalDate inputDate; - // In the case of subscription dryRun we set the targetDate to be the effective date of the change itself - if (dryRunSubscriptionSpec != null && dryRunSubscriptionSpec.getEffectiveDate() != null) { - inputDate = dryRunSubscriptionSpec.getEffectiveDate(); - // In case of Invoice dryRun we also allow the special value UPCOMING_INVOICE_TARGET_DATE where the system will automatically - // generate the resulting targetDate for upcoming invoice; in terms of invoice api that maps to passing a null targetDate - } else if (targetDate != null && targetDate.equals(UPCOMING_INVOICE_TARGET_DATE)) { - inputDate = null; - // Finally, in case of Invoice dryRun, we allow a null input date (will default to NOW), or extract the value provided + if (dryRunSubscriptionSpec != null) { + if (DryRunType.UPCOMING_INVOICE.name().equals(dryRunSubscriptionSpec.getDryRunType())) { + inputDate = null; + } else if (DryRunType.SUBSCRIPTION_ACTION.name().equals(dryRunSubscriptionSpec.getDryRunType()) && dryRunSubscriptionSpec.getEffectiveDate() != null) { + inputDate = dryRunSubscriptionSpec.getEffectiveDate(); + } else { + inputDate = toLocalDate(UUID.fromString(accountId), targetDate, callContext); + } } else { inputDate = toLocalDate(UUID.fromString(accountId), targetDate, callContext); } @@ -458,25 +469,12 @@ public Response createExternalCharges(final Iterable externalCh final CallContext callContext = context.createContext(createdBy, reason, comment, request); final Account account = accountUserApi.getAccountById(UUID.fromString(accountId), callContext); - - // TODO Get rid of that check once we truly support multiple currencies per account - // See discussion https://github.com/killbill/killbill/commit/942e214d49e9c7ed89da76d972ee017d2d3ade58#commitcomment-6045547 - final Set currencies = new HashSet(Lists.transform(ImmutableList.copyOf(externalChargesJson), - new Function() { - @Override - public Currency apply(final InvoiceItemJson input) { - return input.getCurrency(); - } - } - )); - if (currencies.size() != 1 || !currencies.iterator().next().equals(account.getCurrency())) { - throw new InvoiceApiException(ErrorCode.CURRENCY_INVALID, currencies.iterator().next(), account.getCurrency()); - } + final Iterable sanitizedExternalChargesJson = cloneRefundItemsWithValidCurrency(account.getCurrency(), externalChargesJson); // Get the effective date of the external charge, in the account timezone final LocalDate requestedDate = toLocalDate(account, requestedDateTimeString, callContext); - final Iterable externalCharges = Iterables.transform(externalChargesJson, + final Iterable externalCharges = Iterables.transform(sanitizedExternalChargesJson, new Function() { @Override public InvoiceItem apply(final InvoiceItemJson invoiceItemJson) { @@ -492,7 +490,7 @@ public InvoiceItem apply(final InvoiceItemJson invoiceItemJson) { if (!paidInvoices.contains(externalCharge.getInvoiceId())) { paidInvoices.add(externalCharge.getInvoiceId()); final Invoice invoice = invoiceApi.getInvoice(externalCharge.getInvoiceId(), callContext); - createPurchaseForInvoice(account, invoice.getId(), invoice.getBalance(), false, pluginProperties, callContext); + createPurchaseForInvoice(account, invoice.getId(), invoice.getBalance(), account.getPaymentMethodId(), false, pluginProperties, callContext); } } } @@ -508,6 +506,40 @@ public InvoiceItemJson apply(final InvoiceItem input) { return Response.status(Status.OK).entity(createdExternalChargesJson).build(); } + private Iterable cloneRefundItemsWithValidCurrency(final Currency accountCurrency, final Iterable inputItems) throws InvoiceApiException { + try { + return Iterables.transform(inputItems, new Function() { + @Override + public InvoiceItemJson apply(final InvoiceItemJson input) { + if (input.getCurrency() != null) { + if (!input.getCurrency().equals(accountCurrency)) { + throw new IllegalArgumentException(input.getCurrency().toString()); + } + return input; + } else { + return new InvoiceItemJson(null, + input.getInvoiceId(), + null, input.getAccountId(), + input.getBundleId(), + input.getSubscriptionId(), + input.getPlanName(), + input.getPhaseName(), + input.getUsageName(), + input.getItemType(), + input.getDescription(), + input.getStartDate(), + input.getEndDate(), + input.getAmount(), + accountCurrency, + null); + } + } + }); + } catch (IllegalArgumentException e) { + throw new InvoiceApiException(ErrorCode.CURRENCY_INVALID, accountCurrency, e.getMessage()); + } + } + @Timed @GET @Path("/{invoiceId:" + UUID_PATTERN + "}/" + PAYMENTS) @@ -519,21 +551,33 @@ public Response getPayments(@PathParam("invoiceId") final String invoiceId, @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode, @QueryParam(QUERY_WITH_PLUGIN_INFO) @DefaultValue("false") final Boolean withPluginInfo, @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, InvoiceApiException { - final TenantContext tenantContext = context.createContext(request); + final TenantContext tenantContext = context.createContext(request); final Invoice invoice = invoiceApi.getInvoice(UUID.fromString(invoiceId), tenantContext); + + // Extract unique set of paymentId for this invoice + final Set invoicePaymentIds = ImmutableSet.copyOf(Iterables.transform(invoice.getPayments(), new Function() { + @Override + public UUID apply(final InvoicePayment input) { + return input.getPaymentId(); + } + })); + if (invoicePaymentIds.isEmpty()) { + return Response.status(Status.OK).entity(ImmutableList.of()).build(); + } + final List payments = new ArrayList(); - for (InvoicePayment cur : invoice.getPayments()) { - final Payment payment = paymentApi.getPayment(cur.getPaymentId(), withPluginInfo, ImmutableList.of(), tenantContext); + for (final UUID paymentId : invoicePaymentIds) { + final Payment payment = paymentApi.getPayment(paymentId, withPluginInfo, ImmutableList.of(), tenantContext); payments.add(payment); } - final List result = new ArrayList(payments.size()); - if (payments.isEmpty()) { - return Response.status(Status.OK).entity(result).build(); - } - for (final Payment cur : payments) { - result.add(new InvoicePaymentJson(cur, invoice.getId(), null)); - } + + final Iterable result = INVOICE_PAYMENT_ORDERING.sortedCopy(Iterables.transform(payments, new Function() { + @Override + public InvoicePaymentJson apply(final Payment input) { + return new InvoicePaymentJson(input, invoice.getId(), null); + } + })); return Response.status(Status.OK).entity(result).build(); } @@ -557,13 +601,17 @@ public Response createInstantPayment(final InvoicePaymentJson payment, verifyNonNullOrEmpty(payment.getAccountId(), "InvoicePaymentJson accountId needs to be set", payment.getTargetInvoiceId(), "InvoicePaymentJson targetInvoiceId needs to be set", payment.getPurchasedAmount(), "InvoicePaymentJson purchasedAmount needs to be set"); + Preconditions.checkArgument(!externalPayment || payment.getPaymentMethodId() == null, "InvoicePaymentJson should not contain a paymwentMethodId when this is an external payment"); final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString); final CallContext callContext = context.createContext(createdBy, reason, comment, request); final Account account = accountUserApi.getAccountById(UUID.fromString(payment.getAccountId()), callContext); + final UUID paymentMethodId = externalPayment ? null : + (payment.getPaymentMethodId() != null ? UUID.fromString(payment.getPaymentMethodId()) : account.getPaymentMethodId()); + final UUID invoiceId = UUID.fromString(payment.getTargetInvoiceId()); - final Payment result = createPurchaseForInvoice(account, invoiceId, payment.getPurchasedAmount(), externalPayment, pluginProperties, callContext); + final Payment result = createPurchaseForInvoice(account, invoiceId, payment.getPurchasedAmount(), paymentMethodId, externalPayment, pluginProperties, callContext); // STEPH should that live in InvoicePayment instead? return uriBuilder.buildResponse(uriInfo, InvoicePaymentResource.class, "getInvoicePayment", result.getId()); } @@ -899,6 +947,7 @@ protected ObjectType getObjectType() { private static class DefaultDryRunArguments implements DryRunArguments { + private final DryRunType dryRunType; private final SubscriptionEventType action; private final UUID subscriptionId; private final DateTime effectiveDate; @@ -909,6 +958,7 @@ private static class DefaultDryRunArguments implements DryRunArguments { public DefaultDryRunArguments(final InvoiceDryRunJson input, final DateTimeZone accountTimeZone, final Currency currency, final Clock clock) { if (input == null) { + this.dryRunType = DryRunType.TARGET_DATE; this.action = null; this.subscriptionId = null; this.effectiveDate = null; @@ -917,36 +967,42 @@ public DefaultDryRunArguments(final InvoiceDryRunJson input, final DateTimeZone this.billingPolicy = null; this.overrides = null; } else { + this.dryRunType = input.getDryRunType() != null ? DryRunType.valueOf(input.getDryRunType()) : DryRunType.TARGET_DATE; this.action = input.getDryRunAction() != null ? SubscriptionEventType.valueOf(input.getDryRunAction()) : null; this.subscriptionId = input.getSubscriptionId() != null ? UUID.fromString(input.getSubscriptionId()) : null; this.bundleId = input.getBundleId() != null ? UUID.fromString(input.getBundleId()) : null; this.effectiveDate = input.getEffectiveDate() != null ? ClockUtil.computeDateTimeWithUTCReferenceTime(input.getEffectiveDate(), clock.getUTCNow().toLocalTime(), accountTimeZone, clock) : null; this.billingPolicy = input.getBillingPolicy() != null ? BillingActionPolicy.valueOf(input.getBillingPolicy()) : null; - final PlanPhaseSpecifier planPhaseSpecifier = (input.getProductName() != null && - input.getProductCategory() != null && - input.getBillingPeriod() != null) ? - new PlanPhaseSpecifier(input.getProductName(), - ProductCategory.valueOf(input.getProductCategory()), - BillingPeriod.valueOf(input.getBillingPeriod()), - input.getPriceListName(), - input.getPhaseType() != null ? PhaseType.valueOf(input.getPhaseType()) : null) : - null; + final PlanPhaseSpecifier planPhaseSpecifier = (input.getProductName() != null && + input.getProductCategory() != null && + input.getBillingPeriod() != null) ? + new PlanPhaseSpecifier(input.getProductName(), + ProductCategory.valueOf(input.getProductCategory()), + BillingPeriod.valueOf(input.getBillingPeriod()), + input.getPriceListName(), + input.getPhaseType() != null ? PhaseType.valueOf(input.getPhaseType()) : null) : + null; this.specifier = planPhaseSpecifier; this.overrides = input.getPriceOverrides() != null ? ImmutableList.copyOf(Iterables.transform(input.getPriceOverrides(), new Function() { - @Nullable - @Override - public PlanPhasePriceOverride apply(@Nullable final PhasePriceOverrideJson input) { - if (input.getPhaseName() != null) { - return new DefaultPlanPhasePriceOverride(input.getPhaseName(), currency, input.getFixedPrice(), input.getRecurringPrice()); - } else { - return new DefaultPlanPhasePriceOverride(planPhaseSpecifier, currency, input.getFixedPrice(), input.getRecurringPrice()); - } - } - })) : ImmutableList.of(); + @Nullable + @Override + public PlanPhasePriceOverride apply(@Nullable final PhasePriceOverrideJson input) { + if (input.getPhaseName() != null) { + return new DefaultPlanPhasePriceOverride(input.getPhaseName(), currency, input.getFixedPrice(), input.getRecurringPrice()); + } else { + return new DefaultPlanPhasePriceOverride(planPhaseSpecifier, currency, input.getFixedPrice(), input.getRecurringPrice()); + } + } + })) : ImmutableList.of(); } } + @Override + public DryRunType getDryRunType() { + return dryRunType; + } + @Override public PlanPhaseSpecifier getPlanPhaseSpecifier() { return specifier; @@ -978,7 +1034,7 @@ public BillingActionPolicy getBillingActionPolicy() { } @Override - public List getPlanPhasePriceoverrides() { + public List getPlanPhasePriceOverrides() { return overrides; } } diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java index 06581fcdfb..28cd1aa72f 100644 --- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java @@ -54,6 +54,7 @@ import org.killbill.billing.invoice.api.InvoicePaymentType; import org.killbill.billing.jaxrs.json.CustomFieldJson; import org.killbill.billing.jaxrs.json.JsonBase; +import org.killbill.billing.jaxrs.json.PluginPropertyJson; import org.killbill.billing.jaxrs.json.TagJson; import org.killbill.billing.jaxrs.util.Context; import org.killbill.billing.jaxrs.util.JaxrsUriBuilder; @@ -139,6 +140,10 @@ protected ObjectType getObjectType() { protected Response getTags(final UUID accountId, final UUID taggedObjectId, final AuditMode auditMode, final boolean includeDeleted, final TenantContext context) throws TagDefinitionApiException { final List tags = tagUserApi.getTagsForObject(taggedObjectId, getObjectType(), includeDeleted, context); + return createTagResponse(accountId, tags, auditMode, context); + } + + protected Response createTagResponse(final UUID accountId, final List tags, final AuditMode auditMode, final TenantContext context) throws TagDefinitionApiException { final AccountAuditLogsForObjectType tagsAuditLogs = auditUserApi.getAccountAuditLogs(accountId, ObjectType.TAG, auditMode.getLevel(), context); final Map tagDefinitionsCache = new HashMap(); @@ -152,10 +157,10 @@ protected Response getTags(final UUID accountId, final UUID taggedObjectId, fina final List auditLogs = tagsAuditLogs.getAuditLogs(tag.getId()); result.add(new TagJson(tag, tagDefinition, auditLogs)); } - return Response.status(Response.Status.OK).entity(result).build(); } + protected Response createTags(final UUID id, final String tagList, final UriInfo uriInfo, @@ -340,6 +345,20 @@ private LocalDate extractLocalDate(final String inputDate) { return null; } + protected Iterable extractPluginProperties(@Nullable final Iterable pluginProperties) { + return pluginProperties != null ? + Iterables.transform(pluginProperties, + new Function() { + @Override + public PluginProperty apply(final PluginPropertyJson pluginPropertyJson) { + return pluginPropertyJson.toPluginProperty(); + } + } + ) : + ImmutableList.of(); + + } + protected Iterable extractPluginProperties(@Nullable final Iterable pluginProperties, final PluginProperty... additionalProperties) { final Collection properties = new LinkedList(); if (pluginProperties == null) { @@ -348,9 +367,14 @@ protected Iterable extractPluginProperties(@Nullable final Itera for (final String pluginProperty : pluginProperties) { final List property = ImmutableList.copyOf(pluginProperty.split("=")); + // Skip entries for which there is no value + if (property.size() == 1) { + continue; + } + final String key = property.get(0); // Should we URL decode the value? - String value = property.size() == 1 ? null : Joiner.on("=").join(property.subList(1, property.size())); + String value = Joiner.on("=").join(property.subList(1, property.size())); if (pluginProperty.endsWith("=")) { value += "="; } @@ -362,7 +386,7 @@ protected Iterable extractPluginProperties(@Nullable final Itera return properties; } - protected Payment createPurchaseForInvoice(final Account account, final UUID invoiceId, final BigDecimal amountToPay, final Boolean externalPayment, final Iterable pluginProperties, final CallContext callContext) throws PaymentApiException { + protected Payment createPurchaseForInvoice(final Account account, final UUID invoiceId, final BigDecimal amountToPay, final UUID paymentMethodId, final Boolean externalPayment, final Iterable pluginProperties, final CallContext callContext) throws PaymentApiException { final List properties = new ArrayList(); final Iterator pluginPropertyIterator = pluginProperties.iterator(); @@ -376,7 +400,6 @@ protected Payment createPurchaseForInvoice(final Account account, final UUID inv invoiceId.toString(), false); properties.add(invoiceProperty); - final UUID paymentMethodId = externalPayment ? null : account.getPaymentMethodId(); return paymentApi.createPurchaseWithPaymentControl(account, paymentMethodId, null, amountToPay, account.getCurrency(), paymentExternalKey, transactionExternalKey, properties, createInvoicePaymentControlPluginApiPaymentOptions(externalPayment), callContext); } diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java index d29d52774e..0735138d90 100644 --- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java @@ -88,10 +88,13 @@ public interface JaxrsResource { public static final String QUERY_PAYMENT_METHOD_ID = "paymentMethodId"; public static final String QUERY_PAYMENT_CONTROL_PLUGIN_NAME = "controlPluginName"; + public static final String QUERY_TAGS = "tagList"; public static final String QUERY_TAGS_INCLUDED_DELETED = "includedDeleted"; public static final String QUERY_CUSTOM_FIELDS = "customFieldList"; + public static final String QUERY_OBJECT_TYPE = "objectType"; + public static final String QUERY_PAYMENT_METHOD_PLUGIN_NAME = "pluginName"; public static final String QUERY_WITH_PLUGIN_INFO = "withPluginInfo"; public static final String QUERY_PAYMENT_METHOD_IS_DEFAULT = "isDefault"; @@ -113,6 +116,8 @@ public interface JaxrsResource { public static final String QUERY_AUDIT = "audit"; + public static final String QUERY_PARALLEL = "parallel"; + public static final String QUERY_NOTIFICATION_CALLBACK = "cb"; public static final String PAGINATION = "pagination"; @@ -172,6 +177,7 @@ public interface JaxrsResource { public static final String CHARGEBACKS = "chargebacks"; public static final String CHARGEBACKS_PATH = PREFIX + "/" + CHARGEBACKS; + public static final String ALL_TAGS = "allTags"; public static final String TAGS = "tags"; public static final String TAGS_PATH = PREFIX + "/" + TAGS; @@ -221,8 +227,6 @@ public interface JaxrsResource { public static final String INVOICE_TRANSLATION = "translation"; public static final String INVOICE_CATALOG_TRANSLATION = "catalogTranslation"; - public static final String UPCOMING_INVOICE_TARGET_DATE = "upcomingInvoiceTargetDate"; - public static final String COMBO = "combo"; } diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentGatewayResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentGatewayResource.java index 609ef2d53c..11a622125b 100644 --- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentGatewayResource.java +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentGatewayResource.java @@ -1,6 +1,6 @@ /* - * 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 @@ -32,20 +32,19 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -import org.killbill.billing.ErrorCode; import org.killbill.billing.account.api.Account; import org.killbill.billing.account.api.AccountApiException; import org.killbill.billing.account.api.AccountUserApi; +import org.killbill.billing.jaxrs.json.ComboHostedPaymentPageJson; import org.killbill.billing.jaxrs.json.GatewayNotificationJson; import org.killbill.billing.jaxrs.json.HostedPaymentPageFieldsJson; import org.killbill.billing.jaxrs.json.HostedPaymentPageFormDescriptorJson; -import org.killbill.billing.jaxrs.json.PluginPropertyJson; import org.killbill.billing.jaxrs.util.Context; import org.killbill.billing.jaxrs.util.JaxrsUriBuilder; import org.killbill.billing.payment.api.PaymentApi; import org.killbill.billing.payment.api.PaymentApiException; import org.killbill.billing.payment.api.PaymentGatewayApi; -import org.killbill.billing.payment.api.PaymentMethod; +import org.killbill.billing.payment.api.PaymentOptions; import org.killbill.billing.payment.api.PluginProperty; import org.killbill.billing.payment.plugin.api.GatewayNotification; import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor; @@ -56,10 +55,7 @@ import org.killbill.clock.Clock; import com.codahale.metrics.annotation.Timed; -import com.google.common.base.Function; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import com.google.inject.Singleton; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; @@ -72,7 +68,7 @@ @Singleton @Path(JaxrsResource.PAYMENT_GATEWAYS_PATH) @Api(value = JaxrsResource.PAYMENT_GATEWAYS_PATH, description = "HPP endpoints") -public class PaymentGatewayResource extends JaxRsResourceBase { +public class PaymentGatewayResource extends ComboPaymentResource { private final PaymentGatewayApi paymentGatewayApi; @@ -90,6 +86,41 @@ public PaymentGatewayResource(final JaxrsUriBuilder uriBuilder, this.paymentGatewayApi = paymentGatewayApi; } + @Timed + @POST + @Path("/" + HOSTED + "/" + FORM) + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @ApiOperation(value = "Combo API to generate form data to redirect the customer to the gateway", response = HostedPaymentPageFormDescriptorJson.class) + @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid data for Account or PaymentMethod")}) + public Response buildComboFormDescriptor(final ComboHostedPaymentPageJson json, + @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List paymentControlPluginNames, + @QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString, + @HeaderParam(HDR_CREATED_BY) final String createdBy, + @HeaderParam(HDR_REASON) final String reason, + @HeaderParam(HDR_COMMENT) final String comment, + @javax.ws.rs.core.Context final UriInfo uriInfo, + @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException { + verifyNonNullOrEmpty(json, "ComboHostedPaymentPageJson body should be specified"); + + final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString); + final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames); + + final CallContext callContext = context.createContext(createdBy, reason, comment, request); + final Account account = getOrCreateAccount(json.getAccount(), callContext); + + final Iterable paymentMethodPluginProperties = extractPluginProperties(json.getPaymentMethodPluginProperties()); + final UUID paymentMethodId = getOrCreatePaymentMethod(account, json.getPaymentMethod(), paymentMethodPluginProperties, callContext); + + final HostedPaymentPageFieldsJson hostedPaymentPageFields = json.getHostedPaymentPageFieldsJson(); + final Iterable customFields = extractPluginProperties(hostedPaymentPageFields != null ? hostedPaymentPageFields.getCustomFields() : null); + + final HostedPaymentPageFormDescriptor descriptor = paymentGatewayApi.buildFormDescriptorWithPaymentControl(account, paymentMethodId, customFields, pluginProperties, paymentOptions, callContext); + final HostedPaymentPageFormDescriptorJson result = new HostedPaymentPageFormDescriptorJson(descriptor); + + return Response.status(Response.Status.OK).entity(result).build(); + } + @Timed @POST @Path("/" + HOSTED + "/" + FORM + "/{" + QUERY_ACCOUNT_ID + ":" + UUID_PATTERN + "}") @@ -101,6 +132,7 @@ public PaymentGatewayResource(final JaxrsUriBuilder uriBuilder, public Response buildFormDescriptor(final HostedPaymentPageFieldsJson json, @PathParam("accountId") final String accountIdString, @QueryParam(QUERY_PAYMENT_METHOD_ID) final String paymentMethodIdStr, + @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List paymentControlPluginNames, @QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString, @HeaderParam(HDR_CREATED_BY) final String createdBy, @HeaderParam(HDR_REASON) final String reason, @@ -108,6 +140,7 @@ public Response buildFormDescriptor(final HostedPaymentPageFieldsJson json, @javax.ws.rs.core.Context final UriInfo uriInfo, @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException { final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString); + final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames); final CallContext callContext = context.createContext(createdBy, reason, comment, request); final UUID accountId = UUID.fromString(accountIdString); final Account account = accountUserApi.getAccountById(accountId, callContext); @@ -115,26 +148,14 @@ public Response buildFormDescriptor(final HostedPaymentPageFieldsJson json, validatePaymentMethodForAccount(accountId, paymentMethodId, callContext); - final Iterable customFields; - if (json == null) { - customFields = ImmutableList.of(); - } else { - customFields = Iterables.transform(json.getCustomFields(), - new Function() { - @Override - public PluginProperty apply(final PluginPropertyJson pluginPropertyJson) { - return pluginPropertyJson.toPluginProperty(); - } - } - ); - } - final HostedPaymentPageFormDescriptor descriptor = paymentGatewayApi.buildFormDescriptor(account, paymentMethodId, customFields, pluginProperties, callContext); + final Iterable customFields = extractPluginProperties(json.getCustomFields()); + + final HostedPaymentPageFormDescriptor descriptor = paymentGatewayApi.buildFormDescriptorWithPaymentControl(account, paymentMethodId, customFields, pluginProperties, paymentOptions, callContext); final HostedPaymentPageFormDescriptorJson result = new HostedPaymentPageFormDescriptorJson(descriptor); return Response.status(Response.Status.OK).entity(result).build(); } - @Timed @POST @Path("/" + NOTIFICATION + "/{" + QUERY_PAYMENT_PLUGIN_NAME + ":" + ANYTHING_PATTERN + "}") @@ -144,6 +165,7 @@ public PluginProperty apply(final PluginPropertyJson pluginPropertyJson) { @ApiResponses(value = {}) public Response processNotification(final String body, @PathParam(QUERY_PAYMENT_PLUGIN_NAME) final String pluginName, + @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List paymentControlPluginNames, @QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString, @HeaderParam(HDR_CREATED_BY) final String createdBy, @HeaderParam(HDR_REASON) final String reason, @@ -151,6 +173,7 @@ public Response processNotification(final String body, @javax.ws.rs.core.Context final UriInfo uriInfo, @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException { final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString); + final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames); final CallContext callContext = context.createContext(createdBy, reason, comment, request); final String notificationPayload; @@ -161,7 +184,7 @@ public Response processNotification(final String body, } // Note: the body is opaque here, as it comes from the gateway. The associated payment plugin will know how to deserialize it though - final GatewayNotification notification = paymentGatewayApi.processNotification(notificationPayload, pluginName, pluginProperties, callContext); + final GatewayNotification notification = paymentGatewayApi.processNotificationWithPaymentControl(notificationPayload, pluginName, pluginProperties, paymentOptions, callContext); final GatewayNotificationJson result = new GatewayNotificationJson(notification); // The plugin told us how to build the response diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java index 42b942f811..804063d190 100644 --- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java +++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java @@ -1,6 +1,6 @@ /* - * 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 @@ -17,7 +17,9 @@ package org.killbill.billing.jaxrs.resources; +import java.math.BigDecimal; import java.net.URI; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,6 +35,7 @@ import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -46,19 +49,16 @@ import org.killbill.billing.account.api.AccountApiException; import org.killbill.billing.account.api.AccountUserApi; import org.killbill.billing.catalog.api.Currency; -import org.killbill.billing.jaxrs.json.AccountJson; import org.killbill.billing.jaxrs.json.ComboPaymentTransactionJson; import org.killbill.billing.jaxrs.json.PaymentJson; -import org.killbill.billing.jaxrs.json.PaymentMethodJson; import org.killbill.billing.jaxrs.json.PaymentTransactionJson; -import org.killbill.billing.jaxrs.json.PluginPropertyJson; import org.killbill.billing.jaxrs.util.Context; import org.killbill.billing.jaxrs.util.JaxrsUriBuilder; import org.killbill.billing.payment.api.Payment; import org.killbill.billing.payment.api.PaymentApi; import org.killbill.billing.payment.api.PaymentApiException; -import org.killbill.billing.payment.api.PaymentMethod; import org.killbill.billing.payment.api.PaymentOptions; +import org.killbill.billing.payment.api.PaymentTransaction; import org.killbill.billing.payment.api.PluginProperty; import org.killbill.billing.payment.api.TransactionType; import org.killbill.billing.util.api.AuditUserApi; @@ -72,12 +72,10 @@ import com.codahale.metrics.annotation.Timed; import com.google.common.base.Function; -import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; import com.wordnik.swagger.annotations.ApiResponse; @@ -87,7 +85,7 @@ @Path(JaxrsResource.PAYMENTS_PATH) @Api(value = JaxrsResource.PAYMENTS_PATH, description = "Operations on payments") -public class PaymentResource extends JaxRsResourceBase { +public class PaymentResource extends ComboPaymentResource { @Inject public PaymentResource(final JaxrsUriBuilder uriBuilder, @@ -230,6 +228,150 @@ public PaymentJson apply(final Payment payment) { ); } + @Timed + @PUT + @Path("/{paymentId:" + UUID_PATTERN + "}") + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @ApiOperation(value = "Complete an existing transaction") + @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid paymentId supplied"), + @ApiResponse(code = 404, message = "Account or payment not found")}) + public Response completeTransaction(final PaymentTransactionJson json, + @PathParam("paymentId") final String paymentIdStr, + @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List paymentControlPluginNames, + @QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString, + @HeaderParam(HDR_CREATED_BY) final String createdBy, + @HeaderParam(HDR_REASON) final String reason, + @HeaderParam(HDR_COMMENT) final String comment, + @javax.ws.rs.core.Context final UriInfo uriInfo, + @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException { + return completeTransactionInternal(json, paymentIdStr, paymentControlPluginNames, pluginPropertiesString, createdBy, reason, comment, uriInfo, request); + } + + @Timed + @PUT + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @ApiOperation(value = "Complete an existing transaction") + @ApiResponses(value = {@ApiResponse(code = 404, message = "Account or payment not found")}) + public Response completeTransactionByExternalKey(final PaymentTransactionJson json, + @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List paymentControlPluginNames, + @QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString, + @HeaderParam(HDR_CREATED_BY) final String createdBy, + @HeaderParam(HDR_REASON) final String reason, + @HeaderParam(HDR_COMMENT) final String comment, + @javax.ws.rs.core.Context final UriInfo uriInfo, + @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException { + return completeTransactionInternal(json, null, paymentControlPluginNames, pluginPropertiesString, createdBy, reason, comment, uriInfo, request); + } + + private Response completeTransactionInternal(final PaymentTransactionJson json, + @Nullable final String paymentIdStr, + final List paymentControlPluginNames, + final Iterable pluginPropertiesString, + final String createdBy, + final String reason, + final String comment, + final UriInfo uriInfo, + final HttpServletRequest request) throws PaymentApiException, AccountApiException { + final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString); + final CallContext callContext = context.createContext(createdBy, reason, comment, request); + final Payment initialPayment = getPaymentByIdOrKey(paymentIdStr, json == null ? null : json.getPaymentExternalKey(), pluginProperties, callContext); + + final Account account = accountUserApi.getAccountById(initialPayment.getAccountId(), callContext); + final BigDecimal amount = json == null ? null : json.getAmount(); + final Currency currency = json == null || json.getCurrency() == null ? null : Currency.valueOf(json.getCurrency()); + + final TransactionType transactionType; + final String transactionExternalKey; + if (json != null && json.getTransactionId() != null) { + final Collection paymentTransactionCandidates = Collections2.filter(initialPayment.getTransactions(), + new Predicate() { + @Override + public boolean apply(final PaymentTransaction input) { + return input.getId().toString().equals(json.getTransactionId()); + } + }); + if (paymentTransactionCandidates.size() == 1) { + final PaymentTransaction paymentTransaction = paymentTransactionCandidates.iterator().next(); + transactionType = paymentTransaction.getTransactionType(); + transactionExternalKey = paymentTransaction.getExternalKey(); + } else { + return Response.status(Status.NOT_FOUND).build(); + } + } else if (json != null && json.getTransactionExternalKey() != null && json.getTransactionType() != null) { + transactionType = TransactionType.valueOf(json.getTransactionType()); + transactionExternalKey = json.getTransactionExternalKey(); + } else if (json != null && json.getTransactionExternalKey() != null) { + final Collection paymentTransactionCandidates = Collections2.filter(initialPayment.getTransactions(), + new Predicate() { + @Override + public boolean apply(final PaymentTransaction input) { + return input.getExternalKey().equals(json.getTransactionExternalKey()); + } + }); + if (paymentTransactionCandidates.size() == 1) { + transactionType = paymentTransactionCandidates.iterator().next().getTransactionType(); + transactionExternalKey = json.getTransactionExternalKey(); + } else { + // Note: we could bit a bit smarter but keep the logic in the payment system + verifyNonNullOrEmpty(null, "PaymentTransactionJson transactionType needs to be set"); + // Never reached + return Response.status(Status.PRECONDITION_FAILED).build(); + } + } else if (json != null && json.getTransactionType() != null) { + final Collection paymentTransactionCandidates = Collections2.filter(initialPayment.getTransactions(), + new Predicate() { + @Override + public boolean apply(final PaymentTransaction input) { + return input.getTransactionType().toString().equals(json.getTransactionType()); + } + }); + if (paymentTransactionCandidates.size() == 1) { + transactionType = TransactionType.valueOf(json.getTransactionType()); + transactionExternalKey = paymentTransactionCandidates.iterator().next().getExternalKey(); + } else { + verifyNonNullOrEmpty(null, "PaymentTransactionJson externalKey needs to be set"); + // Never reached + return Response.status(Status.PRECONDITION_FAILED).build(); + } + } else if (initialPayment.getTransactions().size() == 1) { + final PaymentTransaction paymentTransaction = initialPayment.getTransactions().get(0); + transactionType = paymentTransaction.getTransactionType(); + transactionExternalKey = paymentTransaction.getExternalKey(); + } else { + verifyNonNullOrEmpty(null, "PaymentTransactionJson transactionType and externalKey need to be set"); + // Never reached + return Response.status(Status.PRECONDITION_FAILED).build(); + } + + final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames); + switch (transactionType) { + case AUTHORIZE: + paymentApi.createAuthorizationWithPaymentControl(account, initialPayment.getPaymentMethodId(), initialPayment.getId(), amount, currency, + initialPayment.getExternalKey(), transactionExternalKey, + pluginProperties, paymentOptions, callContext); + break; + case PURCHASE: + paymentApi.createPurchaseWithPaymentControl(account, initialPayment.getPaymentMethodId(), initialPayment.getId(), amount, currency, + initialPayment.getExternalKey(), transactionExternalKey, + pluginProperties, paymentOptions, callContext); + break; + case CREDIT: + paymentApi.createCreditWithPaymentControl(account, initialPayment.getPaymentMethodId(), initialPayment.getId(), amount, currency, + initialPayment.getExternalKey(), transactionExternalKey, + pluginProperties, paymentOptions, callContext); + break; + case REFUND: + paymentApi.createRefundWithPaymentControl(account, initialPayment.getId(), amount, currency, + transactionExternalKey, pluginProperties, paymentOptions, callContext); + break; + default: + return Response.status(Status.PRECONDITION_FAILED).entity("TransactionType " + transactionType + " cannot be completed").build(); + } + return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", initialPayment.getId()); + } + @Timed @POST @Path("/{paymentId:" + UUID_PATTERN + "}/") @@ -484,17 +626,7 @@ public Response createComboPayment(final ComboPaymentTransactionJson json, final CallContext callContext = context.createContext(createdBy, reason, comment, request); final Account account = getOrCreateAccount(json.getAccount(), callContext); - final Iterable paymentMethodPluginProperties = json.getPaymentMethodPluginProperties() != null ? - Iterables.transform(json.getPaymentMethodPluginProperties(), - new Function() { - @Override - public PluginProperty apply(final PluginPropertyJson pluginPropertyJson) { - return pluginPropertyJson.toPluginProperty(); - } - } - ) : - ImmutableList.of(); - + final Iterable paymentMethodPluginProperties = extractPluginProperties(json.getPaymentMethodPluginProperties()); final UUID paymentMethodId = getOrCreatePaymentMethod(account, json.getPaymentMethod(), paymentMethodPluginProperties, callContext); final PaymentTransactionJson paymentTransactionJson = json.getTransaction(); @@ -502,16 +634,7 @@ public PluginProperty apply(final PluginPropertyJson pluginPropertyJson) { final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames); final Payment result; - final Iterable transactionPluginProperties = json.getTransactionPluginProperties() != null ? - Iterables.transform(json.getTransactionPluginProperties(), - new Function() { - @Override - public PluginProperty apply(final PluginPropertyJson pluginPropertyJson) { - return pluginPropertyJson.toPluginProperty(); - } - } - ) : - ImmutableList.of(); + final Iterable transactionPluginProperties = extractPluginProperties(json.getTransactionPluginProperties()); final Currency currency = paymentTransactionJson.getCurrency() == null ? account.getCurrency() : Currency.valueOf(paymentTransactionJson.getCurrency()); final UUID paymentId = null; // If we need to specify a paymentId (e.g 3DS authorization, we can use regular API, no need for combo call) @@ -541,70 +664,4 @@ public PluginProperty apply(final PluginPropertyJson pluginPropertyJson) { protected ObjectType getObjectType() { return ObjectType.PAYMENT; } - - private Account getOrCreateAccount(final AccountJson accountJson, final CallContext callContext) throws AccountApiException { - // Attempt to retrieve by accountId if specified - if (accountJson.getAccountId() != null) { - return accountUserApi.getAccountById(UUID.fromString(accountJson.getAccountId()), callContext); - } - - if (accountJson.getExternalKey() != null) { - // Attempt to retrieve by account externalKey, ignore if does not exist so we can create it with the key specified. - try { - return accountUserApi.getAccountByKey(accountJson.getExternalKey(), callContext); - } catch (final AccountApiException ignore) {} - } - // Finally create if does not exist - return accountUserApi.createAccount(accountJson.toAccountData(), callContext); - } - - private UUID getOrCreatePaymentMethod(final Account account, final PaymentMethodJson paymentMethodJson, final Iterable pluginProperties, final CallContext callContext) throws PaymentApiException { - - // Get all payment methods for account - final List accountPaymentMethods = paymentApi.getAccountPaymentMethods(account.getId(), false, ImmutableList.of(), callContext); - - // If we were specified a paymentMethod id and we find it, we return it - if (paymentMethodJson.getPaymentMethodId() != null) { - final UUID match = UUID.fromString(paymentMethodJson.getPaymentMethodId()); - if (Iterables.any(accountPaymentMethods, new Predicate() { - @Override - public boolean apply(final PaymentMethod input) { - return input.getId().equals(match); - } - })) { - return match; - } - } - - // If we were specified a paymentMethod externalKey and we find it, we return it - if (paymentMethodJson.getExternalKey() != null) { - final PaymentMethod match = Iterables.tryFind(accountPaymentMethods, new Predicate() { - @Override - public boolean apply(final PaymentMethod input) { - return input.getExternalKey().equals(paymentMethodJson.getExternalKey()); - } - }).orNull(); - if (match != null) { - return match.getId(); - } - } - - // Only set as default if this is the first paymentMethod on the account - final boolean isDefault = accountPaymentMethods.isEmpty(); - final PaymentMethod paymentData = paymentMethodJson.toPaymentMethod(account.getId().toString()); - return paymentApi.addPaymentMethod(account, paymentMethodJson.getExternalKey(), paymentMethodJson.getPluginName(), isDefault, - paymentData.getPluginDetail(), pluginProperties, callContext); - } - - private Payment getPaymentByIdOrKey(@Nullable final String paymentIdStr, @Nullable final String externalKey, final Iterable pluginProperties, final TenantContext tenantContext) throws PaymentApiException { - - Preconditions.checkArgument(paymentIdStr != null || externalKey != null, "Need to set either paymentId or payment externalKey"); - if (paymentIdStr != null) { - final UUID paymentId = UUID.fromString(paymentIdStr); - return paymentApi.getPayment(paymentId, false, pluginProperties, tenantContext); - } else { - return paymentApi.getPaymentByExternalKey(externalKey, false, pluginProperties, tenantContext); - } - } - } diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/resources/TestJaxRsResourceBase.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/resources/TestJaxRsResourceBase.java index 01885d890f..ecf1d4401c 100644 --- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/resources/TestJaxRsResourceBase.java +++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/resources/TestJaxRsResourceBase.java @@ -51,7 +51,17 @@ public void testExtractPluginProperties() throws Exception { Assert.assertEquals(pluginProperties.get(4).getValue(), "2020"); } - private static final class JaxRsResourceBaseTest extends JaxRsResourceBase { + @Test(groups = "fast") + public void testExtractPluginPropertiesWithNullProperty() throws Exception { + final List pluginPropertiesString = ImmutableList.of("foo=", + "bar=ttt"); + final List pluginProperties = ImmutableList.copyOf(base.extractPluginProperties(pluginPropertiesString)); + Assert.assertEquals(pluginProperties.size(), 1); + Assert.assertEquals(pluginProperties.get(0).getKey(), "bar"); + Assert.assertEquals(pluginProperties.get(0).getValue(), "ttt"); + } + + private static final class JaxRsResourceBaseTest extends JaxRsResourceBase { public JaxRsResourceBaseTest() { super(null, null, null, null, null, null, null, null); diff --git a/junction/pom.xml b/junction/pom.xml index 67b286a9f7..c50874ba30 100644 --- a/junction/pom.xml +++ b/junction/pom.xml @@ -19,7 +19,7 @@ killbill org.kill-bill.billing - 0.15.3-SNAPSHOT + 0.15.7-SNAPSHOT ../pom.xml killbill-junction diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java index 24923f7db2..df27149792 100644 --- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java +++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java @@ -21,14 +21,12 @@ import org.joda.time.DateTime; import org.joda.time.DateTimeZone; -import org.killbill.billing.catalog.api.BillingPeriod; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import org.killbill.billing.ErrorCode; -import org.killbill.billing.account.api.Account; import org.killbill.billing.account.api.AccountApiException; +import org.killbill.billing.account.api.ImmutableAccountData; +import org.killbill.billing.callcontext.InternalCallContext; import org.killbill.billing.catalog.api.BillingAlignment; +import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.catalog.api.Catalog; import org.killbill.billing.catalog.api.CatalogApiException; import org.killbill.billing.catalog.api.CatalogService; @@ -37,13 +35,13 @@ import org.killbill.billing.catalog.api.PlanPhase; import org.killbill.billing.catalog.api.PlanPhaseSpecifier; import org.killbill.billing.catalog.api.Product; -import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; -import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException; -import org.killbill.billing.subscription.api.SubscriptionBase; -import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle; -import org.killbill.billing.callcontext.InternalCallContext; import org.killbill.billing.events.EffectiveSubscriptionInternalEvent; +import org.killbill.billing.subscription.api.SubscriptionBase; import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi; +import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; +import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; @@ -61,7 +59,7 @@ public BillCycleDayCalculator(final CatalogService catalogService, final Subscri this.subscriptionApi = subscriptionApi; } - protected int calculateBcd(final UUID bundleId, final SubscriptionBase subscription, final EffectiveSubscriptionInternalEvent transition, final Account account, final InternalCallContext context) + protected int calculateBcd(final ImmutableAccountData account, final int accountBillCycleDayLocal, final UUID bundleId, final SubscriptionBase subscription, final EffectiveSubscriptionInternalEvent transition, final InternalCallContext context) throws CatalogApiException, AccountApiException, SubscriptionBaseApiException { final Catalog catalog = catalogService.getFullCatalog(context); @@ -86,19 +84,16 @@ protected int calculateBcd(final UUID bundleId, final SubscriptionBase subscript phase.getPhaseType()), transition.getRequestedTransitionTime()); - return calculateBcdForAlignment(alignment, bundleId, subscription, account, catalog, plan, context); + return calculateBcdForAlignment(account, accountBillCycleDayLocal, subscription, alignment, bundleId, catalog, plan, context); } @VisibleForTesting - int calculateBcdForAlignment(final BillingAlignment alignment, final UUID bundleId, final SubscriptionBase subscription, - final Account account, final Catalog catalog, final Plan plan, final InternalCallContext context) throws AccountApiException, SubscriptionBaseApiException, CatalogApiException { + int calculateBcdForAlignment(final ImmutableAccountData account, final int accountBillCycleDayLocal, final SubscriptionBase subscription, final BillingAlignment alignment, final UUID bundleId, + final Catalog catalog, final Plan plan, final InternalCallContext context) throws AccountApiException, SubscriptionBaseApiException, CatalogApiException { int result = 0; switch (alignment) { case ACCOUNT: - result = account.getBillCycleDayLocal(); - if (result == 0) { - result = calculateBcdFromSubscription(subscription, plan, account, catalog, context); - } + result = accountBillCycleDayLocal != 0 ? accountBillCycleDayLocal : calculateBcdFromSubscription(subscription, plan, account, catalog, context); break; case BUNDLE: final SubscriptionBase baseSub = subscriptionApi.getBaseSubscription(bundleId, context); @@ -122,7 +117,7 @@ int calculateBcdForAlignment(final BillingAlignment alignment, final UUID bundle } @VisibleForTesting - int calculateBcdFromSubscription(final SubscriptionBase subscription, final Plan plan, final Account account, final Catalog catalog, final InternalCallContext context) + int calculateBcdFromSubscription(final SubscriptionBase subscription, final Plan plan, final ImmutableAccountData account, final Catalog catalog, final InternalCallContext context) throws AccountApiException, CatalogApiException { // Retrieve the initial phase type for that subscription // TODO - this should be extracted somewhere, along with this code above diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java index 270e706d94..c6ee181ca0 100644 --- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java +++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java @@ -32,7 +32,6 @@ import org.killbill.billing.account.api.Account; import org.killbill.billing.callcontext.InternalTenantContext; -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.catalog.api.Plan; @@ -90,8 +89,6 @@ public void insertBlockingEvents(final SortedSet billingEvents, fi return; } - final Account account = billingEvents.first().getAccount(); - final Hashtable> bundleMap = createBundleSubscriptionMap(billingEvents); final SortedSet billingEventsToAdd = new TreeSet(); @@ -101,7 +98,7 @@ public void insertBlockingEvents(final SortedSet billingEvents, fi final List blockingDurations = createBlockingDurations(blockingEvents); for (final UUID bundleId : bundleMap.keySet()) { for (final SubscriptionBase subscription : bundleMap.get(bundleId)) { - billingEventsToAdd.addAll(createNewEvents(blockingDurations, billingEvents, account, subscription)); + billingEventsToAdd.addAll(createNewEvents(blockingDurations, billingEvents, subscription)); billingEventsToRemove.addAll(eventsToRemove(blockingDurations, billingEvents, subscription)); } } @@ -134,7 +131,7 @@ protected SortedSet eventsToRemove(final List di return result; } - protected SortedSet createNewEvents(final List disabledDuration, final SortedSet billingEvents, final Account account, final SubscriptionBase subscription) { + protected SortedSet createNewEvents(final List disabledDuration, final SortedSet billingEvents, final SubscriptionBase subscription) { final SortedSet result = new TreeSet(); for (final DisabledDuration duration : disabledDuration) { // The first one before the blocked duration @@ -189,7 +186,6 @@ protected SortedSet filter(final SortedSet billingEv } protected BillingEvent createNewDisableEvent(final DateTime odEventTime, final BillingEvent previousEvent) { - final Account account = previousEvent.getAccount(); final int billCycleDay = previousEvent.getBillCycleDayLocal(); final SubscriptionBase subscription = previousEvent.getSubscription(); final DateTime effectiveDate = odEventTime; @@ -204,20 +200,18 @@ protected BillingEvent createNewDisableEvent(final DateTime odEventTime, final B final Currency currency = previousEvent.getCurrency(); final String description = ""; - final BillingMode billingMode = previousEvent.getBillingMode(); final SubscriptionBaseTransitionType type = SubscriptionBaseTransitionType.START_BILLING_DISABLED; final Long totalOrdering = globaltotalOrder.getAndIncrement(); final DateTimeZone tz = previousEvent.getTimeZone(); - return new DefaultBillingEvent(account, subscription, effectiveDate, plan, planPhase, + return new DefaultBillingEvent(subscription, effectiveDate, true, plan, planPhase, fixedPrice, recurringPrice, currency, - billingPeriod, billCycleDay, billingMode, + billingPeriod, billCycleDay, description, totalOrdering, type, tz); } protected BillingEvent createNewReenableEvent(final DateTime odEventTime, final BillingEvent previousEvent) { // All fields are populated with the event state from before the blocking period, for invoice to resume invoicing - final Account account = previousEvent.getAccount(); final int billCycleDay = previousEvent.getBillCycleDayLocal(); final SubscriptionBase subscription = previousEvent.getSubscription(); final DateTime effectiveDate = odEventTime; @@ -227,15 +221,14 @@ protected BillingEvent createNewReenableEvent(final DateTime odEventTime, final final BigDecimal recurringPrice = previousEvent.getRecurringPrice(); final Currency currency = previousEvent.getCurrency(); final String description = ""; - final BillingMode billingMode = previousEvent.getBillingMode(); final BillingPeriod billingPeriod = previousEvent.getBillingPeriod(); final SubscriptionBaseTransitionType type = SubscriptionBaseTransitionType.END_BILLING_DISABLED; final Long totalOrdering = globaltotalOrder.getAndIncrement(); final DateTimeZone tz = previousEvent.getTimeZone(); - return new DefaultBillingEvent(account, subscription, effectiveDate, plan, planPhase, + return new DefaultBillingEvent(subscription, effectiveDate, true, plan, planPhase, fixedPrice, recurringPrice, currency, - billingPeriod, billCycleDay, billingMode, + billingPeriod, billCycleDay, description, totalOrdering, type, tz); } diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java index 54cffc5c65..cbf1076187 100644 --- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java +++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java @@ -17,17 +17,13 @@ package org.killbill.billing.junction.plumbing.billing; import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import javax.annotation.Nullable; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; - -import org.killbill.billing.account.api.Account; -import org.killbill.billing.catalog.api.BillingMode; +import org.killbill.billing.account.api.ImmutableAccountData; import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.catalog.api.Catalog; import org.killbill.billing.catalog.api.CatalogApiException; @@ -35,15 +31,16 @@ import org.killbill.billing.catalog.api.Plan; import org.killbill.billing.catalog.api.PlanPhase; import org.killbill.billing.catalog.api.Usage; -import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; -import org.killbill.billing.subscription.api.SubscriptionBase; import org.killbill.billing.events.EffectiveSubscriptionInternalEvent; import org.killbill.billing.junction.BillingEvent; +import org.killbill.billing.subscription.api.SubscriptionBase; +import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; public class DefaultBillingEvent implements BillingEvent { - private final Account account; + private final int billCycleDayLocal; private final SubscriptionBase subscription; private final DateTime effectiveDate; @@ -53,7 +50,6 @@ public class DefaultBillingEvent implements BillingEvent { private final BigDecimal recurringPrice; private final Currency currency; private final String description; - private final BillingMode billingMode; private final BillingPeriod billingPeriod; private final SubscriptionBaseTransitionType type; private final Long totalOrdering; @@ -61,18 +57,17 @@ public class DefaultBillingEvent implements BillingEvent { private final List usages; - public DefaultBillingEvent(final Account account, final EffectiveSubscriptionInternalEvent transition, final SubscriptionBase subscription, final int billCycleDayLocal, final Currency currency, final Catalog catalog) throws CatalogApiException { + public DefaultBillingEvent(final ImmutableAccountData account, final EffectiveSubscriptionInternalEvent transition, final SubscriptionBase subscription, final int billCycleDayLocal, final Currency currency, final Catalog catalog) throws CatalogApiException { + + final boolean isActive = transition.getTransitionType() != SubscriptionBaseTransitionType.CANCEL; - this.account = account; this.billCycleDayLocal = billCycleDayLocal; this.subscription = subscription; this.effectiveDate = transition.getEffectiveTransitionTime(); - final String planPhaseName = (transition.getTransitionType() != SubscriptionBaseTransitionType.CANCEL) ? - transition.getNextPhase() : transition.getPreviousPhase(); + final String planPhaseName = isActive ? transition.getNextPhase() : transition.getPreviousPhase(); this.planPhase = (planPhaseName != null) ? catalog.findPhase(planPhaseName, transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null; - final String planName = (transition.getTransitionType() != SubscriptionBaseTransitionType.CANCEL) ? - transition.getNextPlan() : transition.getPreviousPlan(); + final String planName = isActive ? transition.getNextPlan() : transition.getPreviousPlan(); this.plan = (planName != null) ? catalog.findPlan(planName, transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null; final String nextPhaseName = transition.getNextPhase(); @@ -81,26 +76,23 @@ public DefaultBillingEvent(final Account account, final EffectiveSubscriptionInt final String prevPhaseName = transition.getPreviousPhase(); final PlanPhase prevPhase = (prevPhaseName != null) ? catalog.findPhase(prevPhaseName, transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null; - this.fixedPrice = getFixedPrice(nextPhase, currency); this.recurringPrice = getRecurringPrice(nextPhase, currency); this.currency = currency; this.description = transition.getTransitionType().toString(); - this.billingMode = BillingMode.IN_ADVANCE; - this.billingPeriod = getRecurringBillingPeriod((transition.getTransitionType() != SubscriptionBaseTransitionType.CANCEL) ? nextPhase : prevPhase); + this.billingPeriod = getRecurringBillingPeriod(isActive ? nextPhase : prevPhase); this.type = transition.getTransitionType(); this.totalOrdering = transition.getTotalOrdering(); this.timeZone = account.getTimeZone(); - this.usages = initializeUsage(); + this.usages = initializeUsage(isActive); } - - public DefaultBillingEvent(final Account account, final SubscriptionBase subscription, final DateTime effectiveDate, final Plan plan, final PlanPhase planPhase, + public DefaultBillingEvent(final SubscriptionBase subscription, final DateTime effectiveDate, final boolean isActive, + final Plan plan, final PlanPhase planPhase, final BigDecimal fixedPrice, final BigDecimal recurringPrice, final Currency currency, - final BillingPeriod billingPeriod, final int billCycleDayLocal, final BillingMode billingMode, + final BillingPeriod billingPeriod, final int billCycleDayLocal, final String description, final long totalOrdering, final SubscriptionBaseTransitionType type, final DateTimeZone timeZone) { - this.account = account; this.subscription = subscription; this.effectiveDate = effectiveDate; this.plan = plan; @@ -110,12 +102,11 @@ public DefaultBillingEvent(final Account account, final SubscriptionBase subscri this.currency = currency; this.billingPeriod = billingPeriod; this.billCycleDayLocal = billCycleDayLocal; - this.billingMode = billingMode; this.description = description; this.type = type; this.totalOrdering = totalOrdering; this.timeZone = timeZone; - this.usages = initializeUsage(); + this.usages = initializeUsage(isActive); } @@ -165,11 +156,6 @@ public int compareTo(final BillingEvent e1) { } } - @Override - public Account getAccount() { - return account; - } - @Override public int getBillCycleDayLocal() { return billCycleDayLocal; @@ -200,11 +186,6 @@ public BillingPeriod getBillingPeriod() { return billingPeriod; } - @Override - public BillingMode getBillingMode() { - return billingMode; - } - @Override public String getDescription() { return description; @@ -245,7 +226,6 @@ public List getUsages() { return usages; } - @Override public String toString() { // Note: we don't use all fields here, as the output would be overwhelming @@ -257,7 +237,6 @@ public String toString() { sb.append(", planPhaseName=").append(planPhase.getName()); sb.append(", subscriptionId=").append(subscription.getId()); sb.append(", totalOrdering=").append(totalOrdering); - sb.append(", accountId=").append(account.getId()); sb.append('}'); return sb.toString(); } @@ -276,12 +255,6 @@ public boolean equals(final Object o) { if (billCycleDayLocal != that.billCycleDayLocal) { return false; } - if (account != null ? !account.equals(that.account) : that.account != null) { - return false; - } - if (billingMode != that.billingMode) { - return false; - } if (billingPeriod != that.billingPeriod) { return false; } @@ -324,8 +297,7 @@ public boolean equals(final Object o) { @Override public int hashCode() { - int result = account != null ? account.hashCode() : 0; - result = 31 * result + billCycleDayLocal; + int result = 31 * billCycleDayLocal; result = 31 * result + (subscription != null ? subscription.hashCode() : 0); result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0); result = 31 * result + (planPhase != null ? planPhase.hashCode() : 0); @@ -334,7 +306,6 @@ public int hashCode() { result = 31 * result + (recurringPrice != null ? recurringPrice.hashCode() : 0); result = 31 * result + (currency != null ? currency.hashCode() : 0); result = 31 * result + (description != null ? description.hashCode() : 0); - result = 31 * result + (billingMode != null ? billingMode.hashCode() : 0); result = 31 * result + (billingPeriod != null ? billingPeriod.hashCode() : 0); result = 31 * result + (type != null ? type.hashCode() : 0); result = 31 * result + (totalOrdering != null ? totalOrdering.hashCode() : 0); @@ -342,7 +313,6 @@ public int hashCode() { return result; } - private BigDecimal getFixedPrice(@Nullable final PlanPhase nextPhase, final Currency currency) throws CatalogApiException { return (nextPhase != null && nextPhase.getFixed() != null && nextPhase.getFixed().getPrice() != null) ? nextPhase.getFixed().getPrice().getPrice(currency) : null; } @@ -358,8 +328,11 @@ private BillingPeriod getRecurringBillingPeriod(@Nullable final PlanPhase nextPh return nextPhase.getRecurring() != null ? nextPhase.getRecurring().getBillingPeriod() : BillingPeriod.NO_BILLING_PERIOD; } - private List initializeUsage() { - List result = Collections.emptyList(); + private List initializeUsage(final boolean isActive) { + List result = ImmutableList.of(); + if (!isActive) { + return result; + } if (planPhase != null) { result = Lists.newArrayList(); for (Usage usage : planPhase.getUsages()) { diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEventSet.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEventSet.java index 4586db9d88..fbe8bc8aae 100644 --- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEventSet.java +++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEventSet.java @@ -45,7 +45,7 @@ public class DefaultBillingEventSet extends TreeSet implements Sor private boolean accountAutoInvoiceOff = false; private List subscriptionIdsWithAutoInvoiceOff = new ArrayList(); - private BillingMode recurrringBillingMode; + private BillingMode recurringBillingMode; /* (non-Javadoc) * @see org.killbill.billing.junction.plumbing.billing.BillingEventSet#isAccountAutoInvoiceOff() @@ -57,7 +57,7 @@ public boolean isAccountAutoInvoiceOff() { @Override public BillingMode getRecurringBillingMode() { - return null; + return recurringBillingMode; } /* (non-Javadoc) @@ -90,12 +90,8 @@ public void setAccountAutoInvoiceIsOff(final boolean accountAutoInvoiceIsOff) { this.accountAutoInvoiceOff = accountAutoInvoiceIsOff; } - public BillingMode getRecurrringBillingMode() { - return recurrringBillingMode; - } - - public void setRecurrringBillingMode(final BillingMode recurrringBillingMode) { - this.recurrringBillingMode = recurrringBillingMode; + public void setRecurringBillingMode(final BillingMode recurringBillingMode) { + this.recurringBillingMode = recurringBillingMode; } @Override diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java index 84a3050b19..3fbf992a59 100644 --- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java +++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java @@ -23,10 +23,10 @@ import javax.annotation.Nullable; 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.MutableAccountData; +import org.killbill.billing.account.api.AccountUserApi; +import org.killbill.billing.account.api.ImmutableAccountData; import org.killbill.billing.callcontext.InternalCallContext; import org.killbill.billing.catalog.api.CatalogApiException; import org.killbill.billing.catalog.api.CatalogService; @@ -86,10 +86,10 @@ public BillingEventSet getBillingEventsForAccountAndUpdateAccountBCD(final UUID final List bundles = subscriptionApi.getBundlesForAccount(accountId, context); final DefaultBillingEventSet result = new DefaultBillingEventSet(); final StaticCatalog currentCatalog = catalogService.getCurrentCatalog(context); - result.setRecurrringBillingMode(currentCatalog.getRecurringBillingMode()); + result.setRecurringBillingMode(currentCatalog.getRecurringBillingMode()); try { - final Account account = accountApi.getAccountById(accountId, context); + final ImmutableAccountData account = accountApi.getImmutableAccountDataById(accountId, context); // Check to see if billing is off for the account final List accountTags = tagApi.getTags(accountId, ObjectType.ACCOUNT, context); @@ -123,8 +123,8 @@ private void eventsToString(final StringBuilder stringBuilder, final SortedSet bundles, final Account account, final DryRunArguments dryRunArguments, final InternalCallContext context, - final DefaultBillingEventSet result) throws SubscriptionBaseApiException { + private void addBillingEventsForBundles(final List bundles, final ImmutableAccountData account, final DryRunArguments dryRunArguments, final InternalCallContext context, + final DefaultBillingEventSet result) throws SubscriptionBaseApiException, AccountApiException { final boolean dryRunMode = dryRunArguments != null; @@ -136,14 +136,14 @@ private void addBillingEventsForBundles(final List bundl final UUID fakeBundleId = UUIDs.randomUUID(); final List subscriptions = subscriptionApi.getSubscriptionsForBundle(fakeBundleId, dryRunArguments, context); - addBillingEventsForSubscription(subscriptions, fakeBundleId, account, dryRunMode, context, result); + addBillingEventsForSubscription(account, subscriptions, fakeBundleId, dryRunMode, context, result); } for (final SubscriptionBaseBundle bundle : bundles) { final DryRunArguments dryRunArgumentsForBundle = (dryRunArguments != null && - dryRunArguments.getBundleId() != null && - dryRunArguments.getBundleId().equals(bundle.getId())) ? + dryRunArguments.getBundleId() != null && + dryRunArguments.getBundleId().equals(bundle.getId())) ? dryRunArguments : null; final List subscriptions = subscriptionApi.getSubscriptionsForBundle(bundle.getId(), dryRunArgumentsForBundle, context); @@ -155,18 +155,22 @@ private void addBillingEventsForBundles(final List bundl result.getSubscriptionIdsWithAutoInvoiceOff().add(subscription.getId()); } } else { // billing is not off - addBillingEventsForSubscription(subscriptions, bundle.getId(), account, dryRunMode, context, result); + addBillingEventsForSubscription(account, subscriptions, bundle.getId(), dryRunMode, context, result); } } } - private void addBillingEventsForSubscription(final List subscriptions, final UUID bundleId, final Account account, + private void addBillingEventsForSubscription(final ImmutableAccountData account, + final List subscriptions, + final UUID bundleId, final boolean dryRunMode, final InternalCallContext context, - final DefaultBillingEventSet result) { + final DefaultBillingEventSet result) throws AccountApiException { // If dryRun is specified, we don't want to to update the account BCD value, so we initialize the flag updatedAccountBCD to true boolean updatedAccountBCD = dryRunMode; + + int currentAccountBCD = accountApi.getBCD(account.getId(), context); for (final SubscriptionBase subscription : subscriptions) { // The subscription did not even start, so there is nothing to do yet, we can skip and avoid some NPE down the line when calculating the BCD @@ -176,12 +180,10 @@ private void addBillingEventsForSubscription(final List subscr for (final EffectiveSubscriptionInternalEvent transition : subscriptionApi.getBillingTransitions(subscription, context)) { try { - final int bcdLocal = bcdCalculator.calculateBcd(bundleId, subscription, transition, account, context); + final int bcdLocal = bcdCalculator.calculateBcd(account, currentAccountBCD, bundleId, subscription, transition, context); - if (account.getBillCycleDayLocal() == 0 && !updatedAccountBCD) { - final MutableAccountData modifiedData = account.toMutableAccountData(); - modifiedData.setBillCycleDayLocal(bcdLocal); - accountApi.updateAccount(account.getExternalKey(), modifiedData, context); + if (currentAccountBCD == 0 && !updatedAccountBCD) { + accountApi.updateBCD(account.getExternalKey(), bcdLocal, context); updatedAccountBCD = true; } @@ -190,6 +192,9 @@ private void addBillingEventsForSubscription(final List subscr } catch (CatalogApiException e) { log.error("Failing to identify catalog components while creating BillingEvent from transition: " + transition.getId().toString(), e); + } catch (AccountApiException e) { + // This is unexpected (failed to update BCD) but if this happens we don't want to ignore.. + throw e; } catch (Exception e) { log.warn("Failed while getting BillingEvent", e); } diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java index 5542ff3a0d..adef503dc8 100644 --- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java +++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java @@ -20,11 +20,11 @@ import org.joda.time.DateTime; import org.joda.time.DateTimeZone; +import org.killbill.billing.account.api.ImmutableAccountData; import org.mockito.Mockito; import org.testng.Assert; import org.testng.annotations.Test; -import org.killbill.billing.account.api.Account; import org.killbill.billing.account.api.AccountApiException; import org.killbill.billing.catalog.api.BillingAlignment; import org.killbill.billing.catalog.api.Catalog; @@ -60,10 +60,10 @@ public void testCalculateBCDForAOWithBPCancelledBundleAligned() throws Exception Mockito.when(catalog.findPlan(Mockito.anyString(), Mockito.any(), Mockito.any())).thenReturn(plan); Mockito.when(subscription.getLastActivePlan()).thenReturn(plan); - final Account account = Mockito.mock(Account.class); + final ImmutableAccountData account = Mockito.mock(ImmutableAccountData.class); Mockito.when(account.getTimeZone()).thenReturn(accountTimeZone); - final Integer billCycleDayLocal = billCycleDayCalculator.calculateBcdForAlignment(BillingAlignment.BUNDLE, bundle.getId(), subscription, - account, catalog, null, internalCallContext); + final Integer billCycleDayLocal = billCycleDayCalculator.calculateBcdForAlignment(account, 0, subscription, BillingAlignment.BUNDLE, bundle.getId(), + catalog, null, internalCallContext); Assert.assertEquals(billCycleDayLocal, (Integer) expectedBCDUTC); } @@ -132,7 +132,7 @@ private void verifyBCDCalculation(final DateTimeZone accountTimeZone, final Date final Plan plan = Mockito.mock(Plan.class); Mockito.when(plan.dateOfFirstRecurringNonZeroCharge(startDateUTC, null)).thenReturn(startDateUTC); - final Account account = Mockito.mock(Account.class); + final ImmutableAccountData account = Mockito.mock(ImmutableAccountData.class); Mockito.when(account.getTimeZone()).thenReturn(accountTimeZone); final Integer bcd = billCycleDayCalculator.calculateBcdFromSubscription(subscription, plan, account, Mockito.mock(Catalog.class), internalCallContext); diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java index 4acd3e2cd3..63438b7eea 100644 --- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java +++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java @@ -255,7 +255,6 @@ private void checkEvent(final BillingEvent event, final Plan nextPlan, final int if (!SubscriptionBaseTransitionType.START_BILLING_DISABLED.equals(event.getTransitionType())) { Assert.assertEquals(nextPhase.getRecurring().getBillingPeriod(), event.getBillingPeriod()); } - Assert.assertEquals(BillingMode.IN_ADVANCE, event.getBillingMode()); Assert.assertEquals(desc, event.getTransitionType().toString()); } @@ -266,6 +265,8 @@ private Account createAccount(final int billCycleDay) throws AccountApiException Mockito.when(account.getId()).thenReturn(UUID.randomUUID()); Mockito.when(account.getTimeZone()).thenReturn(DateTimeZone.UTC); Mockito.when(accountInternalApi.getAccountById(Mockito.any(), Mockito.any())).thenReturn(account); + Mockito.when(accountInternalApi.getImmutableAccountDataById(Mockito.any(), Mockito.any())).thenReturn(account); + Mockito.when(accountInternalApi.getBCD(Mockito.any(), Mockito.any())).thenReturn(billCycleDay); return account; } diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java index 867ba4f956..e81a1285fa 100644 --- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java +++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java @@ -28,7 +28,6 @@ import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.LocalDate; -import org.killbill.billing.catalog.api.BillingMode; import org.mockito.Mockito; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -322,7 +321,7 @@ public void testCreateNewEventsOpenPrev() { disabledDuration.add(new DisabledDuration(now, null)); billingEvents.add(createRealEvent(now.minusDays(1), subscription1)); - final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, account, subscription1); + final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, subscription1); assertEquals(results.size(), 1); assertEquals(results.first().getEffectiveDate(), now); @@ -344,7 +343,7 @@ public void testCreateNewEventsOpenPrevFollow() { billingEvents.add(createRealEvent(now.minusDays(1), subscription1)); billingEvents.add(createRealEvent(now.plusDays(1), subscription1)); - final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, account, subscription1); + final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, subscription1); assertEquals(results.size(), 1); assertEquals(results.first().getEffectiveDate(), now); @@ -365,7 +364,7 @@ public void testCreateNewEventsOpenFollow() { disabledDuration.add(new DisabledDuration(now, null)); billingEvents.add(createRealEvent(now.plusDays(1), subscription1)); - final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, account, subscription1); + final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, subscription1); assertEquals(results.size(), 0); } @@ -381,7 +380,7 @@ public void testCreateNewEventsClosedPrev() { disabledDuration.add(new DisabledDuration(now, now.plusDays(2))); billingEvents.add(createRealEvent(now.minusDays(1), subscription1)); - final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, account, subscription1); + final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, subscription1); assertEquals(results.size(), 2); assertEquals(results.first().getEffectiveDate(), now); @@ -406,7 +405,7 @@ public void testCreateNewEventsClosedPrevBetw() { billingEvents.add(createRealEvent(now.minusDays(1), subscription1)); billingEvents.add(createRealEvent(now.plusDays(1), subscription1)); - final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, account, subscription1); + final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, subscription1); assertEquals(results.size(), 2); assertEquals(results.first().getEffectiveDate(), now); @@ -432,7 +431,7 @@ public void testCreateNewEventsClosedPrevBetwNext() { billingEvents.add(createRealEvent(now.plusDays(1), subscription1)); billingEvents.add(createRealEvent(now.plusDays(3), subscription1)); - final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, account, subscription1); + final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, subscription1); assertEquals(results.size(), 2); assertEquals(results.first().getEffectiveDate(), now); @@ -456,7 +455,7 @@ public void testCreateNewEventsClosedBetwn() { disabledDuration.add(new DisabledDuration(now, now.plusDays(2))); billingEvents.add(createRealEvent(now.plusDays(1), subscription1)); - final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, account, subscription1); + final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, subscription1); assertEquals(results.size(), 1); assertEquals(results.last().getEffectiveDate(), now.plusDays(2)); @@ -475,7 +474,7 @@ public void testCreateNewEventsClosedBetweenFollow() { disabledDuration.add(new DisabledDuration(now, now.plusDays(2))); billingEvents.add(createRealEvent(now.plusDays(1), subscription1)); - final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, account, subscription1); + final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, subscription1); assertEquals(results.size(), 1); assertEquals(results.last().getEffectiveDate(), now.plusDays(2)); @@ -494,7 +493,7 @@ public void testCreateNewEventsClosedFollow() { disabledDuration.add(new DisabledDuration(now, now.plusDays(2))); billingEvents.add(createRealEvent(now.plusDays(3), subscription1)); - final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, account, subscription1); + final SortedSet results = blockingCalculator.createNewEvents(disabledDuration, billingEvents, subscription1); assertEquals(results.size(), 0); } @@ -532,14 +531,13 @@ protected BillingEvent createRealEvent(final DateTime effectiveDate, final Subsc final BigDecimal recurringPrice = BigDecimal.TEN; final Currency currency = Currency.USD; final String description = ""; - final BillingMode billingModeType = BillingMode.IN_ADVANCE; final BillingPeriod billingPeriod = BillingPeriod.MONTHLY; final Long totalOrdering = 0L; final DateTimeZone tz = DateTimeZone.UTC; - return new DefaultBillingEvent(account, subscription, effectiveDate, plan, planPhase, + return new DefaultBillingEvent(subscription, effectiveDate, true, plan, planPhase, fixedPrice, recurringPrice, currency, - billingPeriod, billCycleDay, billingModeType, + billingPeriod, billCycleDay, description, totalOrdering, type, tz); } @@ -578,7 +576,6 @@ public void testCreateNewDisableEvent() { assertNull(result.getRecurringPrice()); assertEquals(result.getCurrency(), event.getCurrency()); assertEquals(result.getDescription(), ""); - assertEquals(result.getBillingMode(), event.getBillingMode()); assertEquals(result.getBillingPeriod(), BillingPeriod.NO_BILLING_PERIOD); assertEquals(result.getTransitionType(), SubscriptionBaseTransitionType.START_BILLING_DISABLED); // TODO - ugly, fragile @@ -599,7 +596,6 @@ public void testCreateNewReenableEvent() { assertEquals(result.getRecurringPrice(), event.getRecurringPrice()); assertEquals(result.getCurrency(), event.getCurrency()); assertEquals(result.getDescription(), ""); - assertEquals(result.getBillingMode(), event.getBillingMode()); assertEquals(result.getBillingPeriod(), event.getBillingPeriod()); assertEquals(result.getTransitionType(), SubscriptionBaseTransitionType.END_BILLING_DISABLED); // TODO - ugly, fragile @@ -609,8 +605,8 @@ public void testCreateNewReenableEvent() { private class MockBillingEvent extends DefaultBillingEvent { public MockBillingEvent() { - super(account, subscription1, clock.getUTCNow(), null, null, BigDecimal.ZERO, BigDecimal.TEN, Currency.USD, BillingPeriod.ANNUAL, - 4, BillingMode.IN_ADVANCE, "", 3L, SubscriptionBaseTransitionType.CREATE, DateTimeZone.UTC); + super(subscription1, clock.getUTCNow(), true, null, null, BigDecimal.ZERO, BigDecimal.TEN, Currency.USD, BillingPeriod.ANNUAL, + 4, "", 3L, SubscriptionBaseTransitionType.CREATE, DateTimeZone.UTC); } } diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultBillingEvent.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultBillingEvent.java index 79b50eaceb..aa47d70bb5 100644 --- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultBillingEvent.java +++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultBillingEvent.java @@ -26,7 +26,6 @@ import org.joda.time.DateTime; import org.joda.time.DateTimeZone; -import org.killbill.billing.catalog.api.BillingMode; import org.mockito.Mockito; import org.testng.Assert; import org.testng.annotations.Test; @@ -173,7 +172,7 @@ public void testEventOrderingMix() { public void testToString() throws Exception { // Simple test to ensure we have an easy to read toString representation final BillingEvent event = createEvent(subscription(ID_ZERO), new DateTime("2012-01-01T00:02:04.000Z", DateTimeZone.UTC), SubscriptionBaseTransitionType.CREATE); - Assert.assertEquals(event.toString(), "DefaultBillingEvent{type=CREATE, effectiveDate=2012-01-01T00:02:04.000Z, planPhaseName=Test-trial, subscriptionId=00000000-0000-0000-0000-000000000000, totalOrdering=1, accountId=" + event.getAccount().getId().toString() + "}"); + Assert.assertEquals(event.toString(), "DefaultBillingEvent{type=CREATE, effectiveDate=2012-01-01T00:02:04.000Z, planPhaseName=Test-trial, subscriptionId=00000000-0000-0000-0000-000000000000, totalOrdering=1}"); } private BillingEvent createEvent(final SubscriptionBase sub, final DateTime effectiveDate, final SubscriptionBaseTransitionType type) { @@ -187,10 +186,10 @@ private BillingEvent createEvent(final SubscriptionBase sub, final DateTime effe final PlanPhase shotgunMonthly = createMockMonthlyPlanPhase(null, BigDecimal.ZERO, PhaseType.TRIAL); final Account account = new MockAccountBuilder().build(); - return new DefaultBillingEvent(account, sub, effectiveDate, + return new DefaultBillingEvent(sub, effectiveDate, true, shotgun, shotgunMonthly, BigDecimal.ZERO, null, Currency.USD, BillingPeriod.NO_BILLING_PERIOD, billCycleDay, - BillingMode.IN_ADVANCE, "Test Event 1", totalOrdering, type, DateTimeZone.UTC); + "Test Event 1", totalOrdering, type, DateTimeZone.UTC); } private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate, diff --git a/overdue/pom.xml b/overdue/pom.xml index 8c77086297..c1e0238be9 100644 --- a/overdue/pom.xml +++ b/overdue/pom.xml @@ -19,7 +19,7 @@ killbill org.kill-bill.billing - 0.15.3-SNAPSHOT + 0.15.7-SNAPSHOT ../pom.xml killbill-overdue diff --git a/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueInternalApi.java b/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueInternalApi.java index b5e0d137b0..d676cf9a5e 100644 --- a/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueInternalApi.java +++ b/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueInternalApi.java @@ -18,7 +18,7 @@ import org.killbill.billing.ErrorCode; import org.killbill.billing.ObjectType; -import org.killbill.billing.account.api.Account; +import org.killbill.billing.account.api.ImmutableAccountData; import org.killbill.billing.callcontext.InternalCallContext; import org.killbill.billing.callcontext.InternalTenantContext; import org.killbill.billing.entitlement.api.BlockingStateType; @@ -62,7 +62,7 @@ public DefaultOverdueInternalApi(final OverdueWrapperFactory factory, @SuppressWarnings("unchecked") @Override - public OverdueState getOverdueStateFor(final Account overdueable, final TenantContext context) throws OverdueException { + public OverdueState getOverdueStateFor(final ImmutableAccountData overdueable, final TenantContext context) throws OverdueException { try { final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(context); final String stateName = accessApi.getBlockingStateForService(overdueable.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContextFactory.createInternalTenantContext(context)).getStateName(); @@ -75,7 +75,7 @@ public OverdueState getOverdueStateFor(final Account overdueable, final TenantCo } @Override - public BillingState getBillingStateFor(final Account overdueable, final TenantContext context) throws OverdueException { + public BillingState getBillingStateFor(final ImmutableAccountData overdueable, final TenantContext context) throws OverdueException { log.debug("Billing state of of {} requested", overdueable.getId()); final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(context); @@ -84,19 +84,19 @@ public BillingState getBillingStateFor(final Account overdueable, final TenantCo } @Override - public OverdueState refreshOverdueStateFor(final Account blockable, final CallContext context) throws OverdueException, OverdueApiException { + public OverdueState refreshOverdueStateFor(final ImmutableAccountData blockable, final CallContext context) throws OverdueException, OverdueApiException { log.info("Refresh of blockable {} ({}) requested", blockable.getId(), blockable.getClass()); final InternalCallContext internalCallContext = createInternalCallContext(blockable, context); final OverdueWrapper wrapper = factory.createOverdueWrapperFor(blockable, internalCallContext); return wrapper.refresh(internalCallContext); } - private InternalCallContext createInternalCallContext(final Account blockable, final CallContext context) { + private InternalCallContext createInternalCallContext(final ImmutableAccountData blockable, final CallContext context) { return internalCallContextFactory.createInternalCallContext(blockable.getId(), ObjectType.ACCOUNT, context); } @Override - public void setOverrideBillingStateForAccount(final Account overdueable, final BillingState state, final CallContext context) { + public void setOverrideBillingStateForAccount(final ImmutableAccountData overdueable, final BillingState state, final CallContext context) { throw new UnsupportedOperationException(); } } diff --git a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java index e84cdb6b05..384686ce5e 100644 --- a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java +++ b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java @@ -34,6 +34,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.BillingActionPolicy; @@ -120,7 +121,7 @@ public OverdueStateApplicator(final BlockingInternalApi accessApi, } public void apply(final OverdueStateSet overdueStateSet, final BillingState billingState, - final Account account, final OverdueState previousOverdueState, + final ImmutableAccountData account, final OverdueState previousOverdueState, final OverdueState nextOverdueState, final InternalCallContext context) throws OverdueException, OverdueApiException { try { @@ -157,7 +158,7 @@ public void apply(final OverdueStateSet overdueStateSet, final BillingState bill cancelSubscriptionsIfRequired(account, nextOverdueState, context); - sendEmailIfRequired(billingState, account, nextOverdueState, context); + sendEmailIfRequired(account.getId(), billingState, nextOverdueState, context); avoid_extra_credit_by_toggling_AUTO_INVOICE_OFF(account, previousOverdueState, nextOverdueState, context); @@ -169,6 +170,8 @@ public void apply(final OverdueStateSet overdueStateSet, final BillingState bill if (e.getCode() != ErrorCode.OVERDUE_NO_REEVALUATION_INTERVAL.getCode()) { throw new OverdueException(e); } + } catch (AccountApiException e) { + throw new OverdueException(e); } try { bus.post(createOverdueEvent(account, previousOverdueState.getName(), nextOverdueState.getName(), isBlockBillingTransition(previousOverdueState, nextOverdueState), @@ -178,7 +181,7 @@ public void apply(final OverdueStateSet overdueStateSet, final BillingState bill } } - private void avoid_extra_credit_by_toggling_AUTO_INVOICE_OFF(final Account account, final OverdueState previousOverdueState, + private void avoid_extra_credit_by_toggling_AUTO_INVOICE_OFF(final ImmutableAccountData account, final OverdueState previousOverdueState, final OverdueState nextOverdueState, final InternalCallContext context) throws OverdueApiException { if (isBlockBillingTransition(previousOverdueState, nextOverdueState)) { set_AUTO_INVOICE_OFF_on_blockedBilling(account.getId(), context); @@ -187,7 +190,7 @@ private void avoid_extra_credit_by_toggling_AUTO_INVOICE_OFF(final Account accou } } - public void clear(final Account account, final OverdueState previousOverdueState, final OverdueState clearState, final InternalCallContext context) throws OverdueException { + public void clear(final ImmutableAccountData account, final OverdueState previousOverdueState, final OverdueState clearState, final InternalCallContext context) throws OverdueException { log.debug("OverdueStateApplicator:clear : time = " + clock.getUTCNow() + ", previousState = " + previousOverdueState.getName()); @@ -209,13 +212,13 @@ public void clear(final Account account, final OverdueState previousOverdueState } } - private OverdueChangeInternalEvent createOverdueEvent(final Account overdueable, final String previousOverdueStateName, final String nextOverdueStateName, + private OverdueChangeInternalEvent createOverdueEvent(final ImmutableAccountData overdueable, final String previousOverdueStateName, final String nextOverdueStateName, final boolean isBlockedBilling, final boolean isUnblockedBilling, final InternalCallContext context) throws BlockingApiException { return new DefaultOverdueChangeEvent(overdueable.getId(), previousOverdueStateName, nextOverdueStateName, isBlockedBilling, isUnblockedBilling, context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()); } - protected void storeNewState(final Account blockable, final OverdueState nextOverdueState, final InternalCallContext context) throws OverdueException { + protected void storeNewState(final ImmutableAccountData blockable, final OverdueState nextOverdueState, final InternalCallContext context) throws OverdueException { try { blockingApi.setBlockingState(new DefaultBlockingState(blockable.getId(), BlockingStateType.ACCOUNT, @@ -269,17 +272,17 @@ private boolean blockEntitlement(final OverdueState nextOverdueState) { return nextOverdueState.isDisableEntitlementAndChangesBlocked(); } - protected void createFutureNotification(final Account account, final DateTime timeOfNextCheck, final InternalCallContext context) { + protected void createFutureNotification(final ImmutableAccountData account, final DateTime timeOfNextCheck, final InternalCallContext context) { final OverdueCheckNotificationKey notificationKey = new OverdueCheckNotificationKey(account.getId()); checkPoster.insertOverdueNotification(account.getId(), timeOfNextCheck, OverdueCheckNotifier.OVERDUE_CHECK_NOTIFIER_QUEUE, notificationKey, context); } - protected void clearFutureNotification(final Account account, final InternalCallContext context) { + protected void clearFutureNotification(final ImmutableAccountData account, final InternalCallContext context) { // Need to clear the override table here too (when we add it) checkPoster.clearOverdueCheckNotifications(account.getId(), OverdueCheckNotifier.OVERDUE_CHECK_NOTIFIER_QUEUE, OverdueCheckNotificationKey.class, context); } - private void cancelSubscriptionsIfRequired(final Account account, final OverdueState nextOverdueState, final InternalCallContext context) throws OverdueException { + private void cancelSubscriptionsIfRequired(final ImmutableAccountData account, final OverdueState nextOverdueState, final InternalCallContext context) throws OverdueException { if (nextOverdueState.getOverdueCancellationPolicy() == OverdueCancellationPolicy.NONE) { return; } @@ -315,7 +318,7 @@ private void cancelSubscriptionsIfRequired(final Account account, final OverdueS } } - private void computeEntitlementsToCancel(final Account account, final List result, final CallContext context) throws EntitlementApiException { + private void computeEntitlementsToCancel(final ImmutableAccountData account, final List result, final CallContext context) throws EntitlementApiException { final List allEntitlementsForAccountId = entitlementApi.getAllEntitlementsForAccountId(account.getId(), context); // Entitlement is smart enough and will cancel the associated add-ons. See also discussion in https://github.com/killbill/killbill/issues/94 final Collection allEntitlementsButAddonsForAccountId = Collections2.filter(allEntitlementsForAccountId, @@ -329,8 +332,8 @@ public boolean apply(final Entitlement entitlement) { result.addAll(allEntitlementsButAddonsForAccountId); } - private void sendEmailIfRequired(final BillingState billingState, final Account account, - final OverdueState nextOverdueState, final InternalTenantContext context) { + private void sendEmailIfRequired(final UUID accountId, final BillingState billingState, + final OverdueState nextOverdueState, final InternalTenantContext context) throws AccountApiException { // Note: we don't want to fail the full refresh call because sending the email failed. // That's the reason why we catch all exceptions here. // The alternative would be to: throw new OverdueApiException(e, ErrorCode.EMAIL_SENDING_FAILED); @@ -340,6 +343,7 @@ private void sendEmailIfRequired(final BillingState billingState, final Account return; } + final Account account = accountApi.getAccountById(accountId, context); if (Strings.emptyToNull(account.getEmail()) == null) { log.warn("Unable to send overdue notification email for account {} and overdueable {}: no email specified", account.getId(), account.getId()); return; diff --git a/overdue/src/main/java/org/killbill/billing/overdue/calculator/BillingStateCalculator.java b/overdue/src/main/java/org/killbill/billing/overdue/calculator/BillingStateCalculator.java index 734264b743..7d594fce73 100644 --- a/overdue/src/main/java/org/killbill/billing/overdue/calculator/BillingStateCalculator.java +++ b/overdue/src/main/java/org/killbill/billing/overdue/calculator/BillingStateCalculator.java @@ -26,16 +26,15 @@ import org.joda.time.DateTimeZone; import org.joda.time.LocalDate; - -import org.killbill.billing.account.api.Account; -import org.killbill.billing.payment.api.PaymentResponse; -import org.killbill.clock.Clock; +import org.killbill.billing.account.api.ImmutableAccountData; +import org.killbill.billing.callcontext.InternalTenantContext; import org.killbill.billing.invoice.api.Invoice; +import org.killbill.billing.invoice.api.InvoiceInternalApi; import org.killbill.billing.overdue.config.api.BillingState; import org.killbill.billing.overdue.config.api.OverdueException; -import org.killbill.billing.callcontext.InternalTenantContext; -import org.killbill.billing.invoice.api.InvoiceInternalApi; +import org.killbill.billing.payment.api.PaymentResponse; import org.killbill.billing.util.tag.Tag; +import org.killbill.clock.Clock; import com.google.inject.Inject; @@ -63,7 +62,7 @@ public BillingStateCalculator(final InvoiceInternalApi invoiceApi, final Clock c this.clock = clock; } - public BillingState calculateBillingState(final Account account, final InternalTenantContext context) throws OverdueException { + public BillingState calculateBillingState(final ImmutableAccountData account, final InternalTenantContext context) throws OverdueException { final SortedSet unpaidInvoices = unpaidInvoicesForAccount(account.getId(), account.getTimeZone(), context); final int numberOfUnpaidInvoices = unpaidInvoices.size(); @@ -78,7 +77,6 @@ public BillingState calculateBillingState(final Account account, final InternalT final PaymentResponse responseForLastFailedPayment = PaymentResponse.INSUFFICIENT_FUNDS; //TODO MDW final Tag[] tags = new Tag[]{}; //TODO MDW - return new BillingState(account.getId(), numberOfUnpaidInvoices, unpaidInvoiceBalance, dateOfEarliestUnpaidInvoice, account.getTimeZone(), idOfEarliestUnpaidInvoice, responseForLastFailedPayment, tags); } diff --git a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java index 1439ae5565..0b4560180a 100644 --- a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java +++ b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java @@ -16,7 +16,7 @@ package org.killbill.billing.overdue.wrapper; -import org.killbill.billing.account.api.Account; +import org.killbill.billing.account.api.ImmutableAccountData; import org.killbill.billing.callcontext.InternalCallContext; import org.killbill.billing.callcontext.InternalTenantContext; import org.killbill.billing.entitlement.api.BlockingStateType; @@ -33,14 +33,15 @@ public class OverdueWrapper { - private final Account overdueable; + private final ImmutableAccountData overdueable; private final BlockingInternalApi api; private final Clock clock; private final OverdueStateSet overdueStateSet; private final BillingStateCalculator billingStateCalcuator; private final OverdueStateApplicator overdueStateApplicator; - public OverdueWrapper(final Account overdueable, final BlockingInternalApi api, + public OverdueWrapper(final ImmutableAccountData overdueable, + final BlockingInternalApi api, final OverdueStateSet overdueStateSet, final Clock clock, final BillingStateCalculator billingStateCalcuator, diff --git a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapperFactory.java b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapperFactory.java index c224c83d6f..9c51f85407 100644 --- a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapperFactory.java +++ b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapperFactory.java @@ -19,12 +19,11 @@ import java.util.UUID; import org.joda.time.Period; -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.junction.BlockingInternalApi; -import org.killbill.billing.overdue.OverdueService; import org.killbill.billing.overdue.api.OverdueApiException; import org.killbill.billing.overdue.api.OverdueConfig; import org.killbill.billing.overdue.applicator.OverdueStateApplicator; @@ -65,8 +64,9 @@ public OverdueWrapperFactory(final BlockingInternalApi api, final Clock clock, this.clock = clock; this.overdueConfigCache = overdueConfigCache; } + @SuppressWarnings("unchecked") - public OverdueWrapper createOverdueWrapperFor(final Account blockable, final InternalTenantContext context) throws OverdueException { + public OverdueWrapper createOverdueWrapperFor(final ImmutableAccountData blockable, final InternalTenantContext context) throws OverdueException { return (OverdueWrapper) new OverdueWrapper(blockable, api, getOverdueStateSet(context), clock, billingStateCalculator, overdueStateApplicator); } @@ -75,7 +75,7 @@ public OverdueWrapper createOverdueWrapperFor(final Account blockable, final Int public OverdueWrapper createOverdueWrapperFor(final UUID id, final InternalTenantContext context) throws OverdueException { try { - final Account account = accountApi.getAccountById(id, context); + final ImmutableAccountData account = accountApi.getImmutableAccountDataById(id, context); return new OverdueWrapper(account, api, getOverdueStateSet(context), clock, billingStateCalculator, overdueStateApplicator); } catch (AccountApiException e) { @@ -83,7 +83,6 @@ public OverdueWrapper createOverdueWrapperFor(final UUID id, final InternalTenan } } - private OverdueStateSet getOverdueStateSet(final InternalTenantContext context) throws OverdueException { final OverdueConfig overdueConfig; try { diff --git a/overdue/src/test/java/org/killbill/billing/overdue/TestOverdueHelper.java b/overdue/src/test/java/org/killbill/billing/overdue/TestOverdueHelper.java index b8ebdb2bde..bb01c4dbc4 100644 --- a/overdue/src/test/java/org/killbill/billing/overdue/TestOverdueHelper.java +++ b/overdue/src/test/java/org/killbill/billing/overdue/TestOverdueHelper.java @@ -23,11 +23,11 @@ import org.joda.time.DateTimeZone; import org.joda.time.LocalDate; +import org.killbill.billing.account.api.ImmutableAccountData; import org.killbill.billing.overdue.api.OverdueState; import org.mockito.Mockito; import org.testng.Assert; -import org.killbill.billing.account.api.Account; import org.killbill.billing.account.api.AccountApiException; import org.killbill.billing.invoice.api.Invoice; import org.killbill.billing.invoice.api.InvoiceItem; @@ -116,13 +116,13 @@ public void checkStateApplied(final BlockingState result, final OverdueState sta Assert.assertEquals(result.isBlockBilling(), state.isDisableEntitlementAndChangesBlocked()); } - public Account createAccount(final LocalDate dateOfLastUnPaidInvoice) throws SubscriptionBaseApiException, AccountApiException { + public ImmutableAccountData createImmutableAccountData(final LocalDate dateOfLastUnPaidInvoice) throws SubscriptionBaseApiException, AccountApiException { final UUID accountId = UUID.randomUUID(); - final Account account = Mockito.mock(Account.class); + final ImmutableAccountData account = Mockito.mock(ImmutableAccountData.class); Mockito.when(account.getId()).thenReturn(accountId); Mockito.when(account.getTimeZone()).thenReturn(DateTimeZone.UTC); - Mockito.when(accountInternalApi.getAccountById(Mockito.eq(account.getId()), Mockito.any())).thenReturn(account); + Mockito.when(accountInternalApi.getImmutableAccountDataById(Mockito.eq(account.getId()), Mockito.any())).thenReturn(account); final Invoice invoice = Mockito.mock(Invoice.class); Mockito.when(invoice.getInvoiceDate()).thenReturn(dateOfLastUnPaidInvoice); diff --git a/overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java b/overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java index ac23e0ae4e..5c77f5c09d 100644 --- a/overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java +++ b/overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java @@ -22,13 +22,13 @@ import java.util.UUID; import java.util.concurrent.Callable; +import org.killbill.billing.account.api.ImmutableAccountData; import org.killbill.billing.overdue.api.OverdueState; import org.killbill.billing.overdue.config.DefaultOverdueConfig; import org.mockito.Mockito; import org.testng.Assert; import org.testng.annotations.Test; -import org.killbill.billing.account.api.Account; import org.killbill.billing.overdue.OverdueTestSuiteWithEmbeddedDB; import org.killbill.billing.overdue.config.api.OverdueStateSet; import org.killbill.xmlloader.XMLLoader; @@ -45,7 +45,7 @@ public void testApplicator() throws Exception { final InputStream is = new ByteArrayInputStream(testOverdueHelper.getConfigXml().getBytes()); final DefaultOverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, DefaultOverdueConfig.class); - final Account account = Mockito.mock(Account.class); + final ImmutableAccountData account = Mockito.mock(ImmutableAccountData.class); Mockito.when(account.getId()).thenReturn(UUID.randomUUID()); final OverdueStateSet overdueStateSet = config.getOverdueStatesAccount(); diff --git a/overdue/src/test/java/org/killbill/billing/overdue/calculator/TestBillingStateCalculator.java b/overdue/src/test/java/org/killbill/billing/overdue/calculator/TestBillingStateCalculator.java index 46679929d6..d8f7b9d35c 100644 --- a/overdue/src/test/java/org/killbill/billing/overdue/calculator/TestBillingStateCalculator.java +++ b/overdue/src/test/java/org/killbill/billing/overdue/calculator/TestBillingStateCalculator.java @@ -25,6 +25,7 @@ import org.joda.time.DateTimeZone; import org.joda.time.LocalDate; +import org.killbill.billing.account.api.ImmutableAccountData; import org.mockito.Mockito; import org.testng.Assert; import org.testng.annotations.BeforeMethod; @@ -61,7 +62,7 @@ public BillingStateCalculator createBSCalc() { return new BillingStateCalculator(invoiceApi, clock) { @Override - public BillingState calculateBillingState(final Account overdueable, + public BillingState calculateBillingState(final ImmutableAccountData overdueable, final InternalTenantContext context) { return null; } diff --git a/overdue/src/test/java/org/killbill/billing/overdue/wrapper/TestOverdueWrapper.java b/overdue/src/test/java/org/killbill/billing/overdue/wrapper/TestOverdueWrapper.java index 927af22fca..c6e22312c6 100644 --- a/overdue/src/test/java/org/killbill/billing/overdue/wrapper/TestOverdueWrapper.java +++ b/overdue/src/test/java/org/killbill/billing/overdue/wrapper/TestOverdueWrapper.java @@ -19,7 +19,7 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; -import org.killbill.billing.account.api.Account; +import org.killbill.billing.account.api.ImmutableAccountData; import org.killbill.billing.junction.DefaultBlockingState; import org.killbill.billing.overdue.OverdueTestSuiteWithEmbeddedDB; import org.killbill.billing.overdue.api.OverdueState; @@ -44,24 +44,24 @@ public void testWrapperBasic() throws Exception { final DefaultOverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, DefaultOverdueConfig.class); ((MockOverdueConfigCache) overdueConfigCache).loadOverwriteDefaultOverdueConfig(config); - Account account; + ImmutableAccountData account; OverdueWrapper wrapper; OverdueState state; state = config.getOverdueStatesAccount().findState("OD1"); - account = testOverdueHelper.createAccount(clock.getUTCToday().minusDays(31)); + account = testOverdueHelper.createImmutableAccountData(clock.getUTCToday().minusDays(31)); wrapper = overdueWrapperFactory.createOverdueWrapperFor(account, internalCallContext); wrapper.refresh(internalCallContext); testOverdueHelper.checkStateApplied(state); state = config.getOverdueStatesAccount().findState("OD2"); - account = testOverdueHelper.createAccount(clock.getUTCToday().minusDays(41)); + account = testOverdueHelper.createImmutableAccountData(clock.getUTCToday().minusDays(41)); wrapper = overdueWrapperFactory.createOverdueWrapperFor(account, internalCallContext); wrapper.refresh(internalCallContext); testOverdueHelper.checkStateApplied(state); state = config.getOverdueStatesAccount().findState("OD3"); - account = testOverdueHelper.createAccount(clock.getUTCToday().minusDays(51)); + account = testOverdueHelper.createImmutableAccountData(clock.getUTCToday().minusDays(51)); wrapper = overdueWrapperFactory.createOverdueWrapperFor(account, internalCallContext); wrapper.refresh(internalCallContext); testOverdueHelper.checkStateApplied(state); @@ -70,14 +70,14 @@ public void testWrapperBasic() throws Exception { @Test(groups = "slow") public void testWrapperNoConfig() throws Exception { - final Account account; + final ImmutableAccountData account; final OverdueWrapper wrapper; final OverdueState state; final InputStream is = new ByteArrayInputStream(testOverdueHelper.getConfigXml().getBytes()); final DefaultOverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, DefaultOverdueConfig.class); state = config.getOverdueStatesAccount().findState(DefaultBlockingState.CLEAR_STATE_NAME); - account = testOverdueHelper.createAccount(clock.getUTCToday().minusDays(31)); + account = testOverdueHelper.createImmutableAccountData(clock.getUTCToday().minusDays(31)); wrapper = overdueWrapperFactory.createOverdueWrapperFor(account, internalCallContext); final OverdueState result = wrapper.refresh(internalCallContext); diff --git a/payment/pom.xml b/payment/pom.xml index 83e453a254..659de0a497 100644 --- a/payment/pom.xml +++ b/payment/pom.xml @@ -19,7 +19,7 @@ killbill org.kill-bill.billing - 0.15.3-SNAPSHOT + 0.15.7-SNAPSHOT ../pom.xml killbill-payment diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultApiBase.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultApiBase.java new file mode 100644 index 0000000000..cd916d9c1d --- /dev/null +++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultApiBase.java @@ -0,0 +1,116 @@ +/* + * 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.payment.api; + +import java.math.BigDecimal; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +import javax.annotation.Nullable; + +import org.killbill.billing.ErrorCode; +import org.killbill.billing.account.api.Account; +import org.killbill.billing.catalog.api.Currency; +import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi; +import org.killbill.billing.util.config.PaymentConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableList; + +public class DefaultApiBase { + + private static final Logger log = LoggerFactory.getLogger(DefaultApiBase.class); + + private final PaymentConfig paymentConfig; + + public DefaultApiBase(final PaymentConfig paymentConfig) { + this.paymentConfig = paymentConfig; + } + + protected void logAPICall(final String transactionType, final Account account, final UUID paymentMethodId, @Nullable final UUID paymentId, @Nullable final UUID transactionId, @Nullable final BigDecimal amount, @Nullable final Currency currency, @Nullable final String paymentExternalKey, @Nullable final String paymentTransactionExternalKey) { + if (log.isInfoEnabled()) { + final StringBuilder logLine = new StringBuilder(); + logLine.append("PaymentApi : ") + .append(transactionType) + .append(", account = ") + .append(account.getId()); + if (paymentMethodId != null) { + logLine.append(", paymentMethodId = ") + .append(paymentMethodId); + } + if (paymentExternalKey != null) { + logLine.append(", paymentExternalKey = ") + .append(paymentExternalKey); + } + if (paymentTransactionExternalKey != null) { + logLine.append(", paymentTransactionExternalKey = ") + .append(paymentTransactionExternalKey); + } + if (paymentId != null) { + logLine.append(", paymentId = ") + .append(paymentId); + } + if (transactionId != null) { + logLine.append(", transactionId = ") + .append(transactionId); + } + if (amount != null) { + logLine.append(", amount = ") + .append(amount); + } + if (currency != null) { + logLine.append(", currency = ") + .append(currency); + } + log.info(logLine.toString()); + } + } + + protected List toPaymentControlPluginNames(final PaymentOptions paymentOptions) { + // Special path for JAX-RS InvoicePayment endpoints (see JaxRsResourceBase) + if (paymentConfig.getPaymentControlPluginNames() != null && + paymentOptions.getPaymentControlPluginNames() != null && + paymentOptions.getPaymentControlPluginNames().size() == 1 && + InvoicePaymentControlPluginApi.PLUGIN_NAME.equals(paymentOptions.getPaymentControlPluginNames().get(0))) { + final List paymentControlPluginNames = new LinkedList(paymentOptions.getPaymentControlPluginNames()); + paymentControlPluginNames.addAll(paymentConfig.getPaymentControlPluginNames()); + return paymentControlPluginNames; + } else if (paymentOptions.getPaymentControlPluginNames() != null && !paymentOptions.getPaymentControlPluginNames().isEmpty()) { + return paymentOptions.getPaymentControlPluginNames(); + } else if (paymentConfig.getPaymentControlPluginNames() != null && !paymentConfig.getPaymentControlPluginNames().isEmpty()) { + return paymentConfig.getPaymentControlPluginNames(); + } else { + return ImmutableList.of(); + } + } + + protected void checkNotNullParameter(final Object parameter, final String parameterName) throws PaymentApiException { + if (parameter == null) { + throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, parameterName, "should not be null"); + } + } + + protected void checkPositiveAmount(final BigDecimal amount) throws PaymentApiException { + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, "amount", "should be greater than 0"); + } + } + +} diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java index d329f56878..3da498d432 100644 --- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java +++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java @@ -44,7 +44,7 @@ import com.google.common.collect.ImmutableList; -public class DefaultPaymentApi implements PaymentApi { +public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi { private static final boolean SHOULD_LOCK_ACCOUNT = true; private static final boolean IS_API_PAYMENT = true; @@ -52,7 +52,6 @@ public class DefaultPaymentApi implements PaymentApi { private static final Logger log = LoggerFactory.getLogger(DefaultPaymentApi.class); - private final PaymentConfig paymentConfig; private final PaymentProcessor paymentProcessor; private final PaymentMethodProcessor paymentMethodProcessor; private final PluginControlPaymentProcessor pluginControlPaymentProcessor; @@ -60,7 +59,7 @@ public class DefaultPaymentApi implements PaymentApi { @Inject public DefaultPaymentApi(final PaymentConfig paymentConfig, final PaymentProcessor paymentProcessor, final PaymentMethodProcessor paymentMethodProcessor, final PluginControlPaymentProcessor pluginControlPaymentProcessor, final InternalCallContextFactory internalCallContextFactory) { - this.paymentConfig = paymentConfig; + super(paymentConfig); this.paymentProcessor = paymentProcessor; this.paymentMethodProcessor = paymentMethodProcessor; this.pluginControlPaymentProcessor = pluginControlPaymentProcessor; @@ -72,10 +71,12 @@ public Payment createAuthorization(final Account account, final UUID paymentMeth final Iterable properties, final CallContext callContext) throws PaymentApiException { checkNotNullParameter(account, "account"); checkNotNullParameter(paymentMethodId, "paymentMethodId"); - checkNotNullParameter(amount, "amount"); - checkNotNullParameter(currency, "currency"); + if (paymentId == null) { + checkNotNullParameter(amount, "amount"); + checkPositiveAmount(amount); + checkNotNullParameter(currency, "currency"); + } checkNotNullParameter(properties, "plugin properties"); - checkPositiveAmount(amount); logAPICall(TransactionType.AUTHORIZE.name(), account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey); @@ -95,10 +96,12 @@ public Payment createAuthorizationWithPaymentControl(final Account account, fina checkNotNullParameter(account, "account"); checkNotNullParameter(paymentMethodId, "paymentMethodId"); - checkNotNullParameter(amount, "amount"); - checkNotNullParameter(currency, "currency"); + if (paymentId == null) { + checkNotNullParameter(amount, "amount"); + checkPositiveAmount(amount); + checkNotNullParameter(currency, "currency"); + } checkNotNullParameter(properties, "plugin properties"); - checkPositiveAmount(amount); logAPICall(TransactionType.AUTHORIZE.name(), account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey); @@ -148,10 +151,12 @@ public Payment createPurchase(final Account account, final UUID paymentMethodId, final Iterable properties, final CallContext callContext) throws PaymentApiException { checkNotNullParameter(account, "account"); checkNotNullParameter(paymentMethodId, "paymentMethodId"); - checkNotNullParameter(amount, "amount"); - checkNotNullParameter(currency, "currency"); + if (paymentId == null) { + checkNotNullParameter(amount, "amount"); + checkPositiveAmount(amount); + checkNotNullParameter(currency, "currency"); + } checkNotNullParameter(properties, "plugin properties"); - checkPositiveAmount(amount); logAPICall(TransactionType.PURCHASE.name(), account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey); @@ -161,7 +166,7 @@ public Payment createPurchase(final Account account, final UUID paymentMethodId, } @Override - public Payment createPurchaseWithPaymentControl(final Account account, @Nullable final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency, final String paymentExternalKey, final String paymentTransactionExternalKey, + public Payment createPurchaseWithPaymentControl(final Account account, @Nullable final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey, final Iterable properties, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException { final List paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions); if (paymentControlPluginNames.isEmpty()) { @@ -169,12 +174,13 @@ public Payment createPurchaseWithPaymentControl(final Account account, @Nullable } checkNotNullParameter(account, "account"); - checkNotNullParameter(amount, "amount"); - checkNotNullParameter(currency, "currency"); - checkNotNullParameter(paymentExternalKey, "paymentExternalKey"); + if (paymentId == null) { + checkNotNullParameter(amount, "amount"); + checkPositiveAmount(amount); + checkNotNullParameter(currency, "currency"); + } checkNotNullParameter(paymentTransactionExternalKey, "paymentTransactionExternalKey"); checkNotNullParameter(properties, "plugin properties"); - checkPositiveAmount(amount); logAPICall(TransactionType.PURCHASE.name(), account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey); @@ -227,15 +233,17 @@ public Payment createVoidWithPaymentControl(final Account account, final UUID pa } @Override - public Payment createRefund(final Account account, final UUID paymentId, final BigDecimal amount, final Currency currency, @Nullable final String paymentTransactionExternalKey, final Iterable properties, + public Payment createRefund(final Account account, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency, @Nullable final String paymentTransactionExternalKey, final Iterable properties, final CallContext callContext) throws PaymentApiException { - checkNotNullParameter(account, "account"); - checkNotNullParameter(amount, "amount"); - checkNotNullParameter(currency, "currency"); + if (paymentId == null) { + checkNotNullParameter(currency, "currency"); + } checkNotNullParameter(paymentId, "paymentId"); checkNotNullParameter(properties, "plugin properties"); - checkPositiveAmount(amount); + if (amount != null) { + checkPositiveAmount(amount); + } logAPICall(TransactionType.REFUND.name(), account, null, paymentId, null, amount, currency, null, paymentTransactionExternalKey); @@ -245,7 +253,7 @@ public Payment createRefund(final Account account, final UUID paymentId, final B } @Override - public Payment createRefundWithPaymentControl(final Account account, final UUID paymentId, @Nullable final BigDecimal amount, final Currency currency, final String paymentTransactionExternalKey, final Iterable properties, + public Payment createRefundWithPaymentControl(final Account account, @Nullable final UUID paymentId, @Nullable final BigDecimal amount, final Currency currency, final String paymentTransactionExternalKey, final Iterable properties, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException { final List paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions); if (paymentControlPluginNames.isEmpty()) { @@ -253,7 +261,9 @@ public Payment createRefundWithPaymentControl(final Account account, final UUID } checkNotNullParameter(account, "account"); - checkNotNullParameter(currency, "currency"); + if (paymentId == null) { + checkNotNullParameter(currency, "currency"); + } checkNotNullParameter(paymentId, "paymentId"); checkNotNullParameter(paymentTransactionExternalKey, "paymentTransactionExternalKey"); checkNotNullParameter(properties, "plugin properties"); @@ -275,10 +285,12 @@ public Payment createCredit(final Account account, final UUID paymentMethodId, @ final Iterable properties, final CallContext callContext) throws PaymentApiException { checkNotNullParameter(account, "account"); checkNotNullParameter(paymentMethodId, "paymentMethodId"); - checkNotNullParameter(amount, "amount"); - checkNotNullParameter(currency, "currency"); + if (paymentId == null) { + checkNotNullParameter(amount, "amount"); + checkPositiveAmount(amount); + checkNotNullParameter(currency, "currency"); + } checkNotNullParameter(properties, "plugin properties"); - checkPositiveAmount(amount); logAPICall(TransactionType.CREDIT.name(), account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey); @@ -299,10 +311,12 @@ public Payment createCreditWithPaymentControl(final Account account, final UUID checkNotNullParameter(account, "account"); checkNotNullParameter(paymentMethodId, "paymentMethodId"); - checkNotNullParameter(amount, "amount"); - checkNotNullParameter(currency, "currency"); + if (paymentId == null) { + checkNotNullParameter(amount, "amount"); + checkPositiveAmount(amount); + checkNotNullParameter(currency, "currency"); + } checkNotNullParameter(properties, "plugin properties"); - checkPositiveAmount(amount); logAPICall(TransactionType.CREDIT.name(), account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey); @@ -482,73 +496,4 @@ public List refreshPaymentMethods(final Account account, final It return paymentMethods; } - - private void logAPICall(final String transactionType, final Account account, final UUID paymentMethodId, @Nullable final UUID paymentId, @Nullable final UUID transactionId, @Nullable final BigDecimal amount, @Nullable final Currency currency, @Nullable final String paymentExternalKey, @Nullable final String paymentTransactionExternalKey) { - if (log.isInfoEnabled()) { - final StringBuilder logLine = new StringBuilder(); - logLine.append("PaymentApi : ") - .append(transactionType) - .append(", account = ") - .append(account.getId()); - if (paymentMethodId != null) { - logLine.append(", paymentMethodId = ") - .append(paymentMethodId); - } - if (paymentExternalKey != null) { - logLine.append(", paymentExternalKey = ") - .append(paymentExternalKey); - } - if (paymentTransactionExternalKey != null) { - logLine.append(", paymentTransactionExternalKey = ") - .append(paymentTransactionExternalKey); - } - if (paymentId != null) { - logLine.append(", paymentId = ") - .append(paymentId); - } - if (transactionId != null) { - logLine.append(", transactionId = ") - .append(transactionId); - } - if (amount != null) { - logLine.append(", amount = ") - .append(amount); - } - if (currency != null) { - logLine.append(", currency = ") - .append(currency); - } - log.info(logLine.toString()); - } - } - - private List toPaymentControlPluginNames(final PaymentOptions paymentOptions) { - // Special path for JAX-RS InvoicePayment endpoints (see JaxRsResourceBase) - if (paymentConfig.getPaymentControlPluginNames() != null && - paymentOptions.getPaymentControlPluginNames() != null && - paymentOptions.getPaymentControlPluginNames().size() == 1 && - InvoicePaymentControlPluginApi.PLUGIN_NAME.equals(paymentOptions.getPaymentControlPluginNames().get(0))) { - final List paymentControlPluginNames = new LinkedList(paymentOptions.getPaymentControlPluginNames()); - paymentControlPluginNames.addAll(paymentConfig.getPaymentControlPluginNames()); - return paymentControlPluginNames; - } else if (paymentOptions.getPaymentControlPluginNames() != null && !paymentOptions.getPaymentControlPluginNames().isEmpty()) { - return paymentOptions.getPaymentControlPluginNames(); - } else if (paymentConfig.getPaymentControlPluginNames() != null && !paymentConfig.getPaymentControlPluginNames().isEmpty()) { - return paymentConfig.getPaymentControlPluginNames(); - } else { - return ImmutableList.of(); - } - } - - private void checkNotNullParameter(final Object parameter, final String parameterName) throws PaymentApiException { - if (parameter == null) { - throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, parameterName, "should not be null"); - } - } - - private void checkPositiveAmount(final BigDecimal amount) throws PaymentApiException { - if (amount.compareTo(BigDecimal.ZERO) <= 0) { - throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, "amount", "should be greater than 0"); - } - } } diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java index c9adbaf313..39117a5bee 100644 --- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java +++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java @@ -17,6 +17,7 @@ package org.killbill.billing.payment.api; +import java.util.List; import java.util.UUID; import javax.annotation.Nullable; @@ -24,25 +25,37 @@ import org.killbill.billing.ErrorCode; import org.killbill.billing.account.api.Account; +import org.killbill.billing.control.plugin.api.HPPType; +import org.killbill.billing.control.plugin.api.PaymentApiType; +import org.killbill.billing.control.plugin.api.PaymentControlApiException; +import org.killbill.billing.control.plugin.api.PriorPaymentControlResult; import org.killbill.billing.payment.core.PaymentGatewayProcessor; +import org.killbill.billing.payment.core.sm.control.ControlPluginRunner; import org.killbill.billing.payment.plugin.api.GatewayNotification; import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor; import org.killbill.billing.util.callcontext.CallContext; import org.killbill.billing.util.callcontext.InternalCallContextFactory; +import org.killbill.billing.util.config.PaymentConfig; -public class DefaultPaymentGatewayApi implements PaymentGatewayApi { +public class DefaultPaymentGatewayApi extends DefaultApiBase implements PaymentGatewayApi { private final PaymentGatewayProcessor paymentGatewayProcessor; + private final ControlPluginRunner controlPluginRunner; private final InternalCallContextFactory internalCallContextFactory; @Inject - public DefaultPaymentGatewayApi(final PaymentGatewayProcessor paymentGatewayProcessor, final InternalCallContextFactory internalCallContextFactory) { + public DefaultPaymentGatewayApi(final PaymentConfig paymentConfig, + final PaymentGatewayProcessor paymentGatewayProcessor, + final ControlPluginRunner controlPluginRunner, + final InternalCallContextFactory internalCallContextFactory) { + super(paymentConfig); this.paymentGatewayProcessor = paymentGatewayProcessor; + this.controlPluginRunner = controlPluginRunner; this.internalCallContextFactory = internalCallContextFactory; } @Override - public HostedPaymentPageFormDescriptor buildFormDescriptor(final Account account, @Nullable final UUID paymentMethodId, final Iterable customFields, final Iterable properties, final CallContext callContext) throws PaymentApiException { + public HostedPaymentPageFormDescriptor buildFormDescriptor(final Account account, final UUID paymentMethodId, final Iterable customFields, final Iterable properties, final CallContext callContext) throws PaymentApiException { final UUID paymentMethodIdToUse = paymentMethodId != null ? paymentMethodId : account.getPaymentMethodId(); if (paymentMethodId == null) { @@ -53,8 +66,14 @@ public HostedPaymentPageFormDescriptor buildFormDescriptor(final Account account } @Override - public HostedPaymentPageFormDescriptor buildFormDescriptorWithPaymentControl(final Account account, final UUID uuid, final Iterable iterable, final Iterable iterable1, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException { - throw new IllegalStateException("Not implemented"); + public HostedPaymentPageFormDescriptor buildFormDescriptorWithPaymentControl(final Account account, final UUID paymentMethodId, final Iterable customFields, final Iterable properties, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException { + + return executeWithPaymentControl(account, paymentMethodId, properties, paymentOptions, callContext, new WithPaymentControlCallback() { + @Override + public HostedPaymentPageFormDescriptor doPaymentGatewayApiOperation(final Iterable adjustedPluginProperties) throws PaymentApiException { + return buildFormDescriptor(account, paymentMethodId, customFields, adjustedPluginProperties, callContext); + } + }); } @Override @@ -63,7 +82,61 @@ public GatewayNotification processNotification(final String notification, final } @Override - public GatewayNotification processNotificationWithPaymentControl(final String s, final String s1, final Iterable iterable, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException { - throw new IllegalStateException("Not implemented"); + public GatewayNotification processNotificationWithPaymentControl(final String notification, final String pluginName, final Iterable properties, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException { + return executeWithPaymentControl(null, null, properties, paymentOptions, callContext, new WithPaymentControlCallback() { + @Override + public GatewayNotification doPaymentGatewayApiOperation(final Iterable adjustedPluginProperties) throws PaymentApiException { + return processNotification(notification, pluginName, adjustedPluginProperties, callContext); + } + }); + } + + + private interface WithPaymentControlCallback { + T doPaymentGatewayApiOperation(final Iterable adjustedPluginProperties) throws PaymentApiException; + } + + private T executeWithPaymentControl(@Nullable final Account account, + @Nullable final UUID paymentMethodId, + final Iterable properties, + final PaymentOptions paymentOptions, + final CallContext callContext, + final WithPaymentControlCallback callback) throws PaymentApiException { + + final List paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions); + if (paymentControlPluginNames.isEmpty()) { + return callback.doPaymentGatewayApiOperation(properties); + } + + final PriorPaymentControlResult priorCallResult; + try { + priorCallResult = controlPluginRunner.executePluginPriorCalls(account, + paymentMethodId, + null, null, null, null, + PaymentApiType.HPP, null, HPPType.BUILD_FORM_DESCRIPTOR, + null, null, true, paymentControlPluginNames, properties, callContext); + + } catch (final PaymentControlApiException e) { + throw new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_EXCEPTION, e); + } + + try { + final T result = callback.doPaymentGatewayApiOperation(priorCallResult.getAdjustedPluginProperties()); + controlPluginRunner.executePluginOnSuccessCalls(account, + paymentMethodId, + null, null, null, null, null, + PaymentApiType.HPP, null, HPPType.BUILD_FORM_DESCRIPTOR, + null, null, null, null, true, paymentControlPluginNames, priorCallResult.getAdjustedPluginProperties(), callContext); + return result; + } catch (final PaymentApiException e) { + controlPluginRunner.executePluginOnFailureCalls(account, + paymentMethodId, + null, null, null, null, + PaymentApiType.HPP, null, HPPType.BUILD_FORM_DESCRIPTOR, + null, null, true, paymentControlPluginNames, priorCallResult.getAdjustedPluginProperties(), callContext); + + throw e; + + } } } diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentExecutors.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentExecutors.java new file mode 100644 index 0000000000..bd4ca08499 --- /dev/null +++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentExecutors.java @@ -0,0 +1,99 @@ +/* + * 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.payment.core; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; + +import org.killbill.billing.util.config.PaymentConfig; +import org.killbill.commons.concurrent.Executors; +import org.killbill.commons.concurrent.WithProfilingThreadPoolExecutor; + +public class PaymentExecutors { + + private static final long TIMEOUT_EXECUTOR_SEC = 3L; + + private static final String PLUGIN_THREAD_PREFIX = "Plugin-th-"; + private static final String PAYMENT_PLUGIN_TH_GROUP_NAME = "pay-plugin-grp"; + + public static final String JANITOR_EXECUTOR_NAMED = "JanitorExecutor"; + public static final String PLUGIN_EXECUTOR_NAMED = "PluginExecutor"; + + private final PaymentConfig paymentConfig; + + private volatile ExecutorService pluginExecutorService; + private volatile ScheduledExecutorService janitorExecutorService; + + @Inject + public PaymentExecutors(PaymentConfig paymentConfig) { + this.paymentConfig = paymentConfig; + + } + + public void initialize() { + this.pluginExecutorService = createPluginExecutorService(); + this.janitorExecutorService = createJanitorExecutorService(); + } + + + public void stop() throws InterruptedException { + pluginExecutorService.shutdownNow(); + janitorExecutorService.shutdownNow(); + + pluginExecutorService.awaitTermination(TIMEOUT_EXECUTOR_SEC, TimeUnit.SECONDS); + pluginExecutorService = null; + + janitorExecutorService.awaitTermination(TIMEOUT_EXECUTOR_SEC, TimeUnit.SECONDS); + janitorExecutorService = null; + } + + public ExecutorService getPluginExecutorService() { + return pluginExecutorService; + } + + public ScheduledExecutorService getJanitorExecutorService() { + return janitorExecutorService; + } + + private ExecutorService createPluginExecutorService() { + return new WithProfilingThreadPoolExecutor(paymentConfig.getPaymentPluginThreadNb(), + paymentConfig.getPaymentPluginThreadNb(), + 0L, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + new ThreadFactory() { + + @Override + public Thread newThread(final Runnable r) { + final Thread th = new Thread(new ThreadGroup(PAYMENT_PLUGIN_TH_GROUP_NAME), r); + th.setName(PLUGIN_THREAD_PREFIX + th.getId()); + return th; + } + }); + + } + + private ScheduledExecutorService createJanitorExecutorService() { + return Executors.newSingleThreadScheduledExecutor("PaymentJanitor"); + } +} diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentGatewayProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentGatewayProcessor.java index c142f184b3..cf9e41fe9d 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentGatewayProcessor.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentGatewayProcessor.java @@ -19,7 +19,6 @@ import java.util.UUID; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import javax.inject.Inject; @@ -39,19 +38,16 @@ import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor; import org.killbill.billing.payment.plugin.api.PaymentPluginApi; import org.killbill.billing.payment.plugin.api.PaymentPluginApiException; +import org.killbill.billing.payment.provider.DefaultNoOpGatewayNotification; +import org.killbill.billing.payment.provider.DefaultNoOpHostedPaymentPageFormDescriptor; import org.killbill.billing.tag.TagInternalApi; import org.killbill.billing.util.callcontext.CallContext; import org.killbill.billing.util.callcontext.InternalCallContextFactory; import org.killbill.billing.util.config.PaymentConfig; import org.killbill.clock.Clock; import org.killbill.commons.locker.GlobalLocker; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.google.common.base.Objects; -import com.google.inject.name.Named; - -import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED; // We don't take any lock here because the call needs to be re-entrant // from the plugin: for example, the BitPay plugin will create the payment during the @@ -63,8 +59,6 @@ public class PaymentGatewayProcessor extends ProcessorBase { private final PluginDispatcher paymentPluginFormDispatcher; private final PluginDispatcher paymentPluginNotificationDispatcher; - private static final Logger log = LoggerFactory.getLogger(PaymentGatewayProcessor.class); - @Inject public PaymentGatewayProcessor(final OSGIServiceRegistration pluginRegistry, final AccountInternalApi accountUserApi, @@ -73,13 +67,13 @@ public PaymentGatewayProcessor(final OSGIServiceRegistration p final PaymentDao paymentDao, final GlobalLocker locker, final PaymentConfig paymentConfig, - @Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor, + final PaymentExecutors executors, final InternalCallContextFactory internalCallContextFactory, final Clock clock) { - super(pluginRegistry, accountUserApi, paymentDao, tagUserApi, locker, executor, internalCallContextFactory, invoiceApi, clock); + super(pluginRegistry, accountUserApi, paymentDao, tagUserApi, locker, internalCallContextFactory, invoiceApi, clock); final long paymentPluginTimeoutSec = TimeUnit.SECONDS.convert(paymentConfig.getPaymentPluginTimeout().getPeriod(), paymentConfig.getPaymentPluginTimeout().getUnit()); - this.paymentPluginFormDispatcher = new PluginDispatcher(paymentPluginTimeoutSec, executor); - this.paymentPluginNotificationDispatcher = new PluginDispatcher(paymentPluginTimeoutSec, executor); + this.paymentPluginFormDispatcher = new PluginDispatcher(paymentPluginTimeoutSec, executors); + this.paymentPluginNotificationDispatcher = new PluginDispatcher(paymentPluginTimeoutSec, executors); } public GatewayNotification processNotification(final String notification, final String pluginName, final Iterable properties, final CallContext callContext) throws PaymentApiException { @@ -90,7 +84,7 @@ public PluginDispatcherReturnType call() throws PaymentApiE final PaymentPluginApi plugin = getPaymentPluginApi(pluginName); try { final GatewayNotification result = plugin.processNotification(notification, properties, callContext); - return PluginDispatcher.createPluginDispatcherReturnType(result); + return PluginDispatcher.createPluginDispatcherReturnType(result == null ? new DefaultNoOpGatewayNotification() : result); } catch (final PaymentPluginApiException e) { throw new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_EXCEPTION, e.getErrorMessage()); } @@ -107,7 +101,7 @@ public PluginDispatcherReturnType call() throws try { final HostedPaymentPageFormDescriptor result = plugin.buildFormDescriptor(account.getId(), customFields, properties, callContext); - return PluginDispatcher.createPluginDispatcherReturnType(result); + return PluginDispatcher.createPluginDispatcherReturnType(result == null ? new DefaultNoOpHostedPaymentPageFormDescriptor(account.getId()) : result); } catch (final RuntimeException e) { throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), "")); } catch (final PaymentPluginApiException e) { diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java index 4babea1d07..2e644a2ff1 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java @@ -23,7 +23,6 @@ import java.util.Collections; import java.util.List; import java.util.UUID; -import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -52,6 +51,7 @@ import org.killbill.billing.payment.provider.DefaultPaymentMethodInfoPlugin; import org.killbill.billing.payment.provider.ExternalPaymentProviderPlugin; import org.killbill.billing.tag.TagInternalApi; +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.callcontext.TenantContext; @@ -69,10 +69,7 @@ import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.inject.Inject; -import com.google.inject.name.Named; -import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED; -import org.killbill.billing.util.UUIDs; import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPagination; import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationFromPlugins; @@ -82,6 +79,8 @@ public class PaymentMethodProcessor extends ProcessorBase { private final PluginDispatcher uuidPluginNotificationDispatcher; + private final PaymentConfig paymentConfig; + @Inject public PaymentMethodProcessor(final OSGIServiceRegistration pluginRegistry, final AccountInternalApi accountInternalApi, @@ -90,12 +89,13 @@ public PaymentMethodProcessor(final OSGIServiceRegistration pl final TagInternalApi tagUserApi, final GlobalLocker locker, final PaymentConfig paymentConfig, - @Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor, + final PaymentExecutors executors, final InternalCallContextFactory internalCallContextFactory, final Clock clock) { - super(pluginRegistry, accountInternalApi, paymentDao, tagUserApi, locker, executor, internalCallContextFactory, invoiceApi, clock); + super(pluginRegistry, accountInternalApi, paymentDao, tagUserApi, locker, internalCallContextFactory, invoiceApi, clock); final long paymentPluginTimeoutSec = TimeUnit.SECONDS.convert(paymentConfig.getPaymentPluginTimeout().getPeriod(), paymentConfig.getPaymentPluginTimeout().getUnit()); - this.uuidPluginNotificationDispatcher = new PluginDispatcher(paymentPluginTimeoutSec, executor); + this.paymentConfig = paymentConfig; + this.uuidPluginNotificationDispatcher = new PluginDispatcher(paymentPluginTimeoutSec, executors); } public UUID addPaymentMethod(final String paymentMethodExternalKey, final String paymentPluginServiceName, final Account account, @@ -105,7 +105,8 @@ public UUID addPaymentMethod(final String paymentMethodExternalKey, final String return dispatchWithExceptionHandling(account, new CallableWithAccountLock(locker, account.getExternalKey(), - new WithAccountLockCallback, PaymentApiException>() { + paymentConfig, + new DispatcherCallback, PaymentApiException>() { @Override public PluginDispatcherReturnType doOperation() throws PaymentApiException { @@ -115,8 +116,15 @@ public PluginDispatcherReturnType doOperation() throws PaymentApiException pluginApi = getPaymentPluginApi(paymentPluginServiceName); pm = new DefaultPaymentMethod(paymentMethodExternalKey, account.getId(), paymentPluginServiceName, paymentMethodProps); pluginApi.addPaymentMethod(account.getId(), pm.getId(), paymentMethodProps, setDefault, properties, callContext); - final PaymentMethodModelDao pmModel = new PaymentMethodModelDao(pm.getId(), pm.getExternalKey(), pm.getCreatedDate(), pm.getUpdatedDate(), - pm.getAccountId(), pm.getPluginName(), pm.isActive()); + + final String actualPaymentMethodExternalKey = retrieveActualPaymentMethodExternalKey(account, pm, pluginApi, properties, callContext, context); + final PaymentMethodModelDao pmModel = new PaymentMethodModelDao(pm.getId(), + actualPaymentMethodExternalKey, + pm.getCreatedDate(), + pm.getUpdatedDate(), + pm.getAccountId(), + pm.getPluginName(), + pm.isActive()); paymentDao.insertPaymentMethod(pmModel, context); if (setDefault) { @@ -134,6 +142,30 @@ public PluginDispatcherReturnType doOperation() throws PaymentApiException uuidPluginNotificationDispatcher); } + private String retrieveActualPaymentMethodExternalKey(final Account account, final PaymentMethod pm, final PaymentPluginApi pluginApi, final Iterable properties, final TenantContext callContext, final InternalCallContext context) { + // If the user specified an external key, use it + if (pm.getExternalKey() != null) { + return pm.getExternalKey(); + } + + // Otherwise, check if the plugin sets an external payment method id + final PaymentMethodPlugin paymentMethodPlugin; + try { + paymentMethodPlugin = pluginApi.getPaymentMethodDetail(account.getId(), pm.getId(), properties, callContext); + } catch (final PaymentPluginApiException e) { + log.warn("Error retrieving payment method " + pm.getId() + " from plugin " + pm.getPluginName(), e); + return null; + } + + if (paymentMethodPlugin != null && paymentMethodPlugin.getExternalPaymentMethodId() != null) { + // An external payment method id is set but make sure it doesn't conflict with an existing one + final String externalKey = paymentMethodPlugin.getExternalPaymentMethodId(); + return paymentDao.getPaymentMethodByExternalKeyIncludedDeleted(externalKey, context) == null ? externalKey : null; + } else { + return null; + } + } + public List getPaymentMethods(final UUID accountId, final boolean withPluginInfo, final Iterable properties, final InternalTenantContext context) throws PaymentApiException { return getPaymentMethods(accountId, withPluginInfo, properties, buildTenantContext(context), context); } @@ -334,7 +366,7 @@ public void deletedPaymentMethod(final Account account, final UUID paymentMethod final Iterable properties, final CallContext callContext, final InternalCallContext context) throws PaymentApiException { try { - new WithAccountLock().processAccountWithLock(locker, account.getExternalKey(), new WithAccountLockCallback, PaymentApiException>() { + new WithAccountLock(paymentConfig).processAccountWithLock(locker, account.getExternalKey(), new DispatcherCallback, PaymentApiException>() { @Override public PluginDispatcherReturnType doOperation() throws PaymentApiException { @@ -377,7 +409,7 @@ public PluginDispatcherReturnType doOperation() throws PaymentApiException public void setDefaultPaymentMethod(final Account account, final UUID paymentMethodId, final Iterable properties, final CallContext callContext, final InternalCallContext context) throws PaymentApiException { try { - new WithAccountLock().processAccountWithLock(locker, account.getExternalKey(), new WithAccountLockCallback, PaymentApiException>() { + new WithAccountLock(paymentConfig).processAccountWithLock(locker, account.getExternalKey(), new DispatcherCallback, PaymentApiException>() { @Override public PluginDispatcherReturnType doOperation() throws PaymentApiException { @@ -441,7 +473,7 @@ public List refreshPaymentMethods(final String pluginName, final } try { - final PluginDispatcherReturnType> result = new WithAccountLock, PaymentApiException>().processAccountWithLock(locker, account.getExternalKey(), new WithAccountLockCallback>, PaymentApiException>() { + final PluginDispatcherReturnType> result = new WithAccountLock, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getExternalKey(), new DispatcherCallback>, PaymentApiException>() { @Override public PluginDispatcherReturnType> doOperation() throws PaymentApiException { diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java index 53e1606408..7b39cf1b64 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.concurrent.ExecutorService; import javax.annotation.Nullable; import javax.inject.Inject; @@ -75,9 +74,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; -import com.google.inject.name.Named; -import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED; import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPagination; import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationFromPlugins; @@ -98,11 +95,10 @@ public PaymentProcessor(final OSGIServiceRegistration pluginRe final PaymentDao paymentDao, final InternalCallContextFactory internalCallContextFactory, final GlobalLocker locker, - @Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor, final PaymentAutomatonRunner paymentAutomatonRunner, final IncompletePaymentTransactionTask incompletePaymentTransactionTask, final Clock clock) { - super(pluginRegistry, accountUserApi, paymentDao, tagUserApi, locker, executor, internalCallContextFactory, invoiceApi, clock); + super(pluginRegistry, accountUserApi, paymentDao, tagUserApi, locker, internalCallContextFactory, invoiceApi, clock); this.paymentAutomatonRunner = paymentAutomatonRunner; this.incompletePaymentTransactionTask = incompletePaymentTransactionTask; } @@ -386,7 +382,7 @@ private Payment toPayment(final PaymentModelDao paymentModelDao, @Nullable final final InternalTenantContext tenantContextWithAccountRecordId = getInternalTenantContextWithAccountRecordId(paymentModelDao.getAccountId(), tenantContext); final List transactionsForPayment = paymentDao.getTransactionsForPayment(paymentModelDao.getId(), tenantContextWithAccountRecordId); - return toPayment(paymentModelDao, transactionsForPayment, pluginTransactions, tenantContext); + return toPayment(paymentModelDao, transactionsForPayment, pluginTransactions, tenantContextWithAccountRecordId); } // Used in bulk get API (getAccountPayments) diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentTransactionInfoPluginConverter.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentTransactionInfoPluginConverter.java index bcdc703205..62d7ccdc5b 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentTransactionInfoPluginConverter.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentTransactionInfoPluginConverter.java @@ -17,12 +17,13 @@ package org.killbill.billing.payment.core; -import javax.annotation.Nullable; - import org.killbill.automaton.OperationResult; import org.killbill.billing.payment.api.TransactionStatus; +import org.killbill.billing.payment.plugin.api.PaymentPluginStatus; import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; +import com.google.common.base.MoreObjects; + // // Conversion between the plugin result to the payment state and transaction status // @@ -30,7 +31,8 @@ public class PaymentTransactionInfoPluginConverter { public static TransactionStatus toTransactionStatus(final PaymentTransactionInfoPlugin paymentTransactionInfoPlugin) { - switch (paymentTransactionInfoPlugin.getStatus()) { + final PaymentPluginStatus status = MoreObjects.firstNonNull(paymentTransactionInfoPlugin.getStatus(), PaymentPluginStatus.UNDEFINED); + switch (status) { case PROCESSED: return TransactionStatus.SUCCESS; case PENDING: @@ -54,7 +56,8 @@ public static TransactionStatus toTransactionStatus(final PaymentTransactionInfo } public static OperationResult toOperationResult(final PaymentTransactionInfoPlugin paymentTransactionInfoPlugin) { - switch (paymentTransactionInfoPlugin.getStatus()) { + final PaymentPluginStatus status = MoreObjects.firstNonNull(paymentTransactionInfoPlugin.getStatus(), PaymentPluginStatus.UNDEFINED); + switch (status) { case PROCESSED: return OperationResult.SUCCESS; case PENDING: diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java index fd66fe91fe..96a6eda749 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java @@ -21,7 +21,6 @@ import java.util.Collection; import java.util.List; import java.util.UUID; -import java.util.concurrent.ExecutorService; import javax.annotation.Nullable; import javax.inject.Inject; @@ -55,9 +54,6 @@ import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; -import com.google.inject.name.Named; - -import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED; public class PluginControlPaymentProcessor extends ProcessorBase { @@ -73,12 +69,11 @@ public PluginControlPaymentProcessor(final OSGIServiceRegistration pluginRegistry; protected final AccountInternalApi accountInternalApi; protected final GlobalLocker locker; - protected final ExecutorService executor; protected final PaymentDao paymentDao; protected final InternalCallContextFactory internalCallContextFactory; protected final TagInternalApi tagInternalApi; @@ -86,7 +79,6 @@ public ProcessorBase(final OSGIServiceRegistration pluginRegis final PaymentDao paymentDao, final TagInternalApi tagInternalApi, final GlobalLocker locker, - final ExecutorService executor, final InternalCallContextFactory internalCallContextFactory, final InvoiceInternalApi invoiceApi, final Clock clock) { @@ -94,7 +86,6 @@ public ProcessorBase(final OSGIServiceRegistration pluginRegis this.accountInternalApi = accountInternalApi; this.paymentDao = paymentDao; this.locker = locker; - this.executor = executor; this.tagInternalApi = tagInternalApi; this.internalCallContextFactory = internalCallContextFactory; this.invoiceApi = invoiceApi; @@ -164,8 +155,7 @@ protected CallContext buildCallContext(final InternalCallContext context) { return internalCallContextFactory.createCallContext(context); } - // TODO Rename - there is no lock! - public interface WithAccountLockCallback { + public interface DispatcherCallback { public PluginDispatcherReturnType doOperation() throws ExceptionType; } @@ -173,29 +163,38 @@ public static class CallableWithAccountLock, ExceptionType> callback; + private final DispatcherCallback, ExceptionType> callback; + private final PaymentConfig paymentConfig; public CallableWithAccountLock(final GlobalLocker locker, final String accountExternalKey, - final WithAccountLockCallback, ExceptionType> callback) { + final PaymentConfig paymentConfig, + final DispatcherCallback, ExceptionType> callback) { this.locker = locker; this.accountExternalKey = accountExternalKey; this.callback = callback; + this.paymentConfig = paymentConfig; } @Override public PluginDispatcherReturnType call() throws ExceptionType, LockFailedException { - return new WithAccountLock().processAccountWithLock(locker, accountExternalKey, callback); + return new WithAccountLock(paymentConfig).processAccountWithLock(locker, accountExternalKey, callback); } } public static class WithAccountLock { - public PluginDispatcherReturnType processAccountWithLock(final GlobalLocker locker, final String accountExternalKey, final WithAccountLockCallback, ExceptionType> callback) + private final PaymentConfig paymentConfig; + + public WithAccountLock(final PaymentConfig paymentConfig) { + this.paymentConfig = paymentConfig; + } + + public PluginDispatcherReturnType processAccountWithLock(final GlobalLocker locker, final String accountExternalKey, final DispatcherCallback, ExceptionType> callback) throws ExceptionType, LockFailedException { GlobalLock lock = null; try { - lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountExternalKey, NB_LOCK_TRY); + lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountExternalKey, paymentConfig.getMaxGlobalLockRetries()); return callback.doOperation(); } finally { if (lock != null) { diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java index 60281b49e0..4803871d01 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java @@ -18,16 +18,14 @@ package org.killbill.billing.payment.core.janitor; import java.io.IOException; -import java.util.List; -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.DefaultCallContext; import org.killbill.billing.callcontext.InternalTenantContext; import org.killbill.billing.events.PaymentInternalEvent; import org.killbill.billing.osgi.api.OSGIServiceRegistration; -import org.killbill.billing.payment.core.ProcessorBase; import org.killbill.billing.payment.core.sm.PaymentControlStateMachineHelper; import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper; import org.killbill.billing.payment.dao.PaymentDao; @@ -123,8 +121,8 @@ public interface JanitorIterationCallback { protected T doJanitorOperationWithAccountLock(final JanitorIterationCallback callback, final InternalTenantContext internalTenantContext) { GlobalLock lock = null; try { - final Account account = accountInternalApi.getAccountByRecordId(internalTenantContext.getAccountRecordId(), internalTenantContext); - lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getExternalKey(), ProcessorBase.NB_LOCK_TRY); + final ImmutableAccountData account = accountInternalApi.getImmutableAccountDataByRecordId(internalTenantContext.getAccountRecordId(), internalTenantContext); + lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getExternalKey(), paymentConfig.getMaxGlobalLockRetries()); return callback.doIteration(); } catch (AccountApiException e) { log.warn(String.format("Janitor failed to retrieve account with recordId %s", internalTenantContext.getAccountRecordId()), e); diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java index e7f2318b2c..9851afb17f 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java @@ -70,11 +70,16 @@ public class IncompletePaymentAttemptTask extends CompletionTaskBase pluginRegistry, final GlobalLocker locker) { + final OSGIServiceRegistration pluginRegistry, + final GlobalLocker locker) { super(internalCallContextFactory, paymentConfig, paymentDao, clock, paymentStateMachineHelper, retrySMHelper, accountInternalApi, pluginRegistry, locker); this.pluginControlledPaymentAutomatonRunner = pluginControlledPaymentAutomatonRunner; } diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java index cdc58a76d5..15dc19137a 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java @@ -112,6 +112,7 @@ public Void doIteration() { rehydratedPaymentTransaction.getCreatedDate(), rehydratedPaymentTransaction.getCreatedDate(), PaymentPluginStatus.UNDEFINED, + null, null); PaymentTransactionInfoPlugin paymentTransactionInfoPlugin; try { @@ -142,7 +143,7 @@ public void processPaymentEvent(final PaymentInternalEvent event, final Notifica if (!TRANSACTION_STATUSES_TO_CONSIDER.contains(event.getStatus())) { return; } - insertNewNotificationForUnresolvedTransactionIfNeeded(event.getPaymentTransactionId(), 1, event.getUserToken(), event.getSearchKey1(), event.getSearchKey2()); + insertNewNotificationForUnresolvedTransactionIfNeeded(event.getPaymentTransactionId(), 0, event.getUserToken(), event.getSearchKey1(), event.getSearchKey2()); } public boolean updatePaymentAndTransactionIfNeededWithAccountLock(final PaymentModelDao payment, final PaymentTransactionModelDao paymentTransaction, final PaymentTransactionInfoPlugin paymentTransactionInfoPlugin, final InternalTenantContext internalTenantContext) { @@ -186,15 +187,27 @@ private boolean updatePaymentAndTransactionInternal(final PaymentModelDao paymen newPaymentState = paymentStateMachineHelper.getFailureStateForTransaction(paymentTransaction.getTransactionType()); break; case PLUGIN_FAILURE: + newPaymentState = paymentStateMachineHelper.getErroredStateForTransaction(paymentTransaction.getTransactionType()); + break; case UNKNOWN: default: - log.info("Janitor IncompletePaymentTransactionTask unable to repair payment {}, transaction {}: {} -> {}", - payment.getId(), paymentTransaction.getId(), paymentTransaction.getTransactionStatus(), transactionStatus); + if (transactionStatus != paymentTransaction.getTransactionStatus()) { + log.info("Janitor IncompletePaymentTransactionTask unable to repair payment {}, transaction {}: {} -> {}", + payment.getId(), paymentTransaction.getId(), paymentTransaction.getTransactionStatus(), transactionStatus); + } // We can't get anything interesting from the plugin... insertNewNotificationForUnresolvedTransactionIfNeeded(paymentTransaction.getId(), attemptNumber, userToken, internalTenantContext.getAccountRecordId(), internalTenantContext.getTenantRecordId()); return false; } + // Our status did not change, so we just insert a new notification (attemptNumber will be incremented) + if (transactionStatus == paymentTransaction.getTransactionStatus()) { + log.debug("Janitor IncompletePaymentTransactionTask repairing payment {}, transaction {}, transitioning transactionStatus from {} -> {}", + payment.getId(), paymentTransaction.getId(), paymentTransaction.getTransactionStatus(), transactionStatus); + insertNewNotificationForUnresolvedTransactionIfNeeded(paymentTransaction.getId(), attemptNumber, userToken, internalTenantContext.getAccountRecordId(), internalTenantContext.getTenantRecordId()); + return false; + } + // Recompute new lastSuccessPaymentState. This is important to be able to allow new operations on the state machine (for e.g an AUTH_SUCCESS would now allow a CAPTURE operation) final String lastSuccessPaymentState = paymentStateMachineHelper.isSuccessState(newPaymentState) ? newPaymentState : null; @@ -208,6 +221,7 @@ private boolean updatePaymentAndTransactionInternal(final PaymentModelDao paymen final String gatewayErrorCode = paymentTransactionInfoPlugin != null ? paymentTransactionInfoPlugin.getGatewayErrorCode() : paymentTransaction.getGatewayErrorCode(); final String gatewayError = paymentTransactionInfoPlugin != null ? paymentTransactionInfoPlugin.getGatewayError() : paymentTransaction.getGatewayErrorMsg(); + log.info("Janitor IncompletePaymentTransactionTask repairing payment {}, transaction {}, transitioning transactionStatus from {} -> {}", payment.getId(), paymentTransaction.getId(), paymentTransaction.getTransactionStatus(), transactionStatus); @@ -238,10 +252,10 @@ private PaymentPluginApi getPaymentPluginApi(final PaymentModelDao item, final S } @VisibleForTesting - DateTime getNextNotificationTime(@Nullable final Integer attemptNumber) { + DateTime getNextNotificationTime(final Integer attemptNumber) { final List retries = paymentConfig.getIncompleteTransactionsRetries(); - if (attemptNumber == null || attemptNumber > retries.size()) { + if (attemptNumber > retries.size()) { return null; } final TimeSpan nextDelay = retries.get(attemptNumber - 1); @@ -249,8 +263,15 @@ DateTime getNextNotificationTime(@Nullable final Integer attemptNumber) { } private void insertNewNotificationForUnresolvedTransactionIfNeeded(final UUID paymentTransactionId, @Nullable final Integer attemptNumber, @Nullable final UUID userToken, final Long accountRecordId, final Long tenantRecordId) { - final NotificationEvent key = new JanitorNotificationKey(paymentTransactionId, IncompletePaymentTransactionTask.class.toString(), attemptNumber); - final DateTime notificationTime = getNextNotificationTime(attemptNumber); + // When we come from a GET path, we don't want to insert a new notification + if (attemptNumber == null) { + return; + } + + // Increment value before we insert + final Integer newAttemptNumber = attemptNumber.intValue() + 1; + final NotificationEvent key = new JanitorNotificationKey(paymentTransactionId, IncompletePaymentTransactionTask.class.toString(), newAttemptNumber); + final DateTime notificationTime = getNextNotificationTime(newAttemptNumber); // Will be null in the GET path or when we run out opf attempts.. if (notificationTime != null) { try { diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java index 3b06de11b6..fbac8e0033 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java @@ -22,13 +22,22 @@ import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import javax.inject.Named; import org.joda.time.DateTime; +import org.killbill.billing.account.api.AccountInternalApi; import org.killbill.billing.events.PaymentInternalEvent; +import org.killbill.billing.osgi.api.OSGIServiceRegistration; +import org.killbill.billing.payment.core.PaymentExecutors; +import org.killbill.billing.payment.core.sm.PaymentControlStateMachineHelper; +import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper; +import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner; +import org.killbill.billing.payment.dao.PaymentDao; import org.killbill.billing.payment.glue.DefaultPaymentService; -import org.killbill.billing.payment.glue.PaymentModule; +import org.killbill.billing.payment.plugin.api.PaymentPluginApi; +import org.killbill.billing.util.callcontext.InternalCallContextFactory; import org.killbill.billing.util.config.PaymentConfig; +import org.killbill.clock.Clock; +import org.killbill.commons.locker.GlobalLocker; import org.killbill.notificationq.api.NotificationEvent; import org.killbill.notificationq.api.NotificationQueue; import org.killbill.notificationq.api.NotificationQueueService; @@ -49,29 +58,76 @@ public class Janitor { public static final String QUEUE_NAME = "janitor"; private final NotificationQueueService notificationQueueService; - private final ScheduledExecutorService janitorExecutor; private final PaymentConfig paymentConfig; - private final IncompletePaymentAttemptTask incompletePaymentAttemptTask; - private final IncompletePaymentTransactionTask incompletePaymentTransactionTask; + private final PaymentExecutors paymentExecutors; + private final Clock clock; + private final PaymentDao paymentDao; + private final InternalCallContextFactory internalCallContextFactory; + private final PaymentStateMachineHelper paymentStateMachineHelper; + private final PaymentControlStateMachineHelper retrySMHelper; + private final AccountInternalApi accountInternalApi; + private final OSGIServiceRegistration pluginRegistry; + private final GlobalLocker locker; + private final PluginControlPaymentAutomatonRunner pluginControlledPaymentAutomatonRunner; + + + + private IncompletePaymentAttemptTask incompletePaymentAttemptTask; + private IncompletePaymentTransactionTask incompletePaymentTransactionTask; private NotificationQueue janitorQueue; + private ScheduledExecutorService janitorExecutor; private volatile boolean isStopped; @Inject - public Janitor(final PaymentConfig paymentConfig, + public Janitor(final InternalCallContextFactory internalCallContextFactory, + final PaymentDao paymentDao, + final Clock clock, + final PaymentStateMachineHelper paymentStateMachineHelper, + final PaymentControlStateMachineHelper retrySMHelper, + final AccountInternalApi accountInternalApi, + final PluginControlPaymentAutomatonRunner pluginControlledPaymentAutomatonRunner, + final OSGIServiceRegistration pluginRegistry, + final GlobalLocker locker, + final PaymentConfig paymentConfig, final NotificationQueueService notificationQueueService, - @Named(PaymentModule.JANITOR_EXECUTOR_NAMED) final ScheduledExecutorService janitorExecutor, - final IncompletePaymentAttemptTask incompletePaymentAttemptTask, - final IncompletePaymentTransactionTask incompletePaymentTransactionTask) { + final PaymentExecutors paymentExecutors) { this.notificationQueueService = notificationQueueService; - this.janitorExecutor = janitorExecutor; + this.paymentExecutors = paymentExecutors; this.paymentConfig = paymentConfig; - this.incompletePaymentAttemptTask = incompletePaymentAttemptTask; - this.incompletePaymentTransactionTask = incompletePaymentTransactionTask; - this.isStopped = false; + this.internalCallContextFactory = internalCallContextFactory; + this.paymentDao = paymentDao; + this.clock = clock; + this.pluginControlledPaymentAutomatonRunner = pluginControlledPaymentAutomatonRunner; + this.paymentStateMachineHelper = paymentStateMachineHelper; + this.retrySMHelper = retrySMHelper; + this.accountInternalApi = accountInternalApi; + this.pluginRegistry = pluginRegistry; + this.locker = locker; + } + /* + public IncompletePaymentAttemptTask(final InternalCallContextFactory internalCallContextFactory, + final PaymentConfig paymentConfig, + final PaymentDao paymentDao, + final Clock clock, + final PaymentStateMachineHelper paymentStateMachineHelper, + final PaymentControlStateMachineHelper retrySMHelper, + final AccountInternalApi accountInternalApi, + final PluginControlPaymentAutomatonRunner pluginControlledPaymentAutomatonRunner, + final OSGIServiceRegistration pluginRegistry, + final GlobalLocker locker) { + + + public IncompletePaymentTransactionTask(final InternalCallContextFactory internalCallContextFactory, final PaymentConfig paymentConfig, + final PaymentDao paymentDao, final Clock clock, + final PaymentStateMachineHelper paymentStateMachineHelper, final PaymentControlStateMachineHelper retrySMHelper, final AccountInternalApi accountInternalApi, + final OSGIServiceRegistration pluginRegistry, final GlobalLocker locker) { + + */ + public void initialize() throws NotificationQueueAlreadyExists { janitorQueue = notificationQueueService.createNotificationQueue(DefaultPaymentService.SERVICE_NAME, QUEUE_NAME, @@ -90,15 +146,38 @@ public void handleReadyNotification(final NotificationEvent notificationKey, fin } } ); + + this.incompletePaymentAttemptTask = new IncompletePaymentAttemptTask(internalCallContextFactory, + paymentConfig, + paymentDao, + clock, + paymentStateMachineHelper, + retrySMHelper, + accountInternalApi, + pluginControlledPaymentAutomatonRunner, + pluginRegistry, + locker); + + this.incompletePaymentTransactionTask = new IncompletePaymentTransactionTask(internalCallContextFactory, + paymentConfig, + paymentDao, + clock, + paymentStateMachineHelper, + retrySMHelper, + accountInternalApi, + pluginRegistry, + locker); + + incompletePaymentTransactionTask.attachJanitorQueue(janitorQueue); incompletePaymentAttemptTask.attachJanitorQueue(janitorQueue); } public void start() { - if (isStopped) { - log.warn("Janitor is not a restartable service, and was already started, aborting"); - return; - } + + this.isStopped = false; + + janitorExecutor = paymentExecutors.getJanitorExecutorService(); janitorQueue.startQueue(); diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java index 8ab58ff559..34c8c1b403 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java @@ -25,9 +25,10 @@ import org.killbill.automaton.OperationResult; import org.killbill.billing.account.api.Account; import org.killbill.billing.payment.core.ProcessorBase.CallableWithAccountLock; -import org.killbill.billing.payment.core.ProcessorBase.WithAccountLockCallback; +import org.killbill.billing.payment.core.ProcessorBase.DispatcherCallback; import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,15 +39,18 @@ public abstract class OperationCallbackBase paymentPluginDispatcher; + private final PaymentConfig paymentConfig; protected final PaymentStateContext paymentStateContext; protected OperationCallbackBase(final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, final PaymentStateContext paymentStateContext) { this.locker = locker; this.paymentPluginDispatcher = paymentPluginDispatcher; this.paymentStateContext = paymentStateContext; + this.paymentConfig = paymentConfig; } // @@ -54,13 +58,14 @@ protected OperationCallbackBase(final GlobalLocker locker, // The dispatcher may throw a TimeoutException, ExecutionException, or InterruptedException; those will be handled in specific // callback to eventually throw a OperationException, that will be used to drive the state machine in the right direction. // - protected OperationResult dispatchWithAccountLockAndTimeout(final WithAccountLockCallback, ExceptionType> callback) throws OperationException { + protected OperationResult dispatchWithAccountLockAndTimeout(final DispatcherCallback, ExceptionType> callback) throws OperationException { final Account account = paymentStateContext.getAccount(); logger.debug("Dispatching plugin call for account {}", account.getExternalKey()); try { final Callable> task = new CallableWithAccountLock(locker, account.getExternalKey(), + paymentConfig, callback); final OperationResult operationResult = paymentPluginDispatcher.dispatchWithTimeout(task); logger.debug("Successful plugin call for account {} with result {}", account.getExternalKey(), operationResult); diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java index 4353d9e01a..49702f5d15 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java @@ -19,7 +19,6 @@ import java.math.BigDecimal; import java.util.UUID; -import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -36,7 +35,6 @@ import org.killbill.automaton.State.EnteringStateCallback; import org.killbill.automaton.State.LeavingStateCallback; import org.killbill.automaton.StateMachine; -import org.killbill.automaton.StateMachineConfig; import org.killbill.billing.ErrorCode; import org.killbill.billing.account.api.Account; import org.killbill.billing.callcontext.InternalCallContext; @@ -45,6 +43,7 @@ import org.killbill.billing.payment.api.PaymentApiException; import org.killbill.billing.payment.api.PluginProperty; import org.killbill.billing.payment.api.TransactionType; +import org.killbill.billing.payment.core.PaymentExecutors; import org.killbill.billing.payment.core.sm.payments.AuthorizeCompleted; import org.killbill.billing.payment.core.sm.payments.AuthorizeInitiated; import org.killbill.billing.payment.core.sm.payments.AuthorizeOperation; @@ -69,7 +68,6 @@ import org.killbill.billing.payment.dao.PaymentDao; import org.killbill.billing.payment.dao.PaymentModelDao; import org.killbill.billing.payment.dispatcher.PluginDispatcher; -import org.killbill.billing.payment.glue.PaymentModule; import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi; import org.killbill.billing.payment.plugin.api.PaymentPluginApi; import org.killbill.billing.util.callcontext.CallContext; @@ -82,9 +80,6 @@ import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; -import com.google.inject.name.Named; - -import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED; public class PaymentAutomatonRunner { @@ -95,15 +90,15 @@ public class PaymentAutomatonRunner { protected final OSGIServiceRegistration pluginRegistry; protected final Clock clock; private final PersistentBus eventBus; + private final PaymentConfig paymentConfig; @Inject - public PaymentAutomatonRunner(@javax.inject.Named(PaymentModule.STATE_MACHINE_PAYMENT) final StateMachineConfig stateMachineConfig, - final PaymentConfig paymentConfig, + public PaymentAutomatonRunner(final PaymentConfig paymentConfig, final PaymentDao paymentDao, final GlobalLocker locker, final OSGIServiceRegistration pluginRegistry, final Clock clock, - @Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor, + final PaymentExecutors executors, final PersistentBus eventBus, final PaymentStateMachineHelper paymentSMHelper) { this.paymentSMHelper = paymentSMHelper; @@ -112,9 +107,9 @@ public PaymentAutomatonRunner(@javax.inject.Named(PaymentModule.STATE_MACHINE_PA this.pluginRegistry = pluginRegistry; this.clock = clock; this.eventBus = eventBus; - + this.paymentConfig = paymentConfig; final long paymentPluginTimeoutSec = TimeUnit.SECONDS.convert(paymentConfig.getPaymentPluginTimeout().getPeriod(), paymentConfig.getPaymentPluginTimeout().getUnit()); - this.paymentPluginDispatcher = new PluginDispatcher(paymentPluginTimeoutSec, executor); + this.paymentPluginDispatcher = new PluginDispatcher(paymentPluginTimeoutSec, executors); } @@ -125,20 +120,23 @@ public UUID run(final boolean isApiPayment, final TransactionType transactionTyp final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException { final DateTime utcNow = clock.getUTCNow(); - final PaymentStateContext paymentStateContext = new PaymentStateContext(isApiPayment, paymentId, transactionId, attemptId, paymentExternalKey, paymentTransactionExternalKey, transactionType, + // Retrieve the payment id from the payment external key if needed + final UUID effectivePaymentId = paymentId != null ? paymentId : retrievePaymentId(paymentExternalKey, internalCallContext); + + final PaymentStateContext paymentStateContext = new PaymentStateContext(isApiPayment, effectivePaymentId, transactionId, attemptId, paymentExternalKey, paymentTransactionExternalKey, transactionType, account, paymentMethodId, amount, currency, shouldLockAccount, overridePluginOperationResult, properties, internalCallContext, callContext); final PaymentAutomatonDAOHelper daoHelper = new PaymentAutomatonDAOHelper(paymentStateContext, utcNow, paymentDao, pluginRegistry, internalCallContext, eventBus, paymentSMHelper); final UUID effectivePaymentMethodId; final String currentStateName; - if (paymentId != null) { + if (effectivePaymentId != null) { final PaymentModelDao paymentModelDao = daoHelper.getPayment(); effectivePaymentMethodId = paymentModelDao.getPaymentMethodId(); currentStateName = paymentModelDao.getLastSuccessStateName() != null ? paymentModelDao.getLastSuccessStateName() : paymentSMHelper.getInitStateNameForTransaction(); // Check for illegal states (should never happen) - Preconditions.checkState(currentStateName != null, "State name cannot be null for payment " + paymentId); + Preconditions.checkState(currentStateName != null, "State name cannot be null for payment " + effectivePaymentId); Preconditions.checkState(paymentMethodId == null || effectivePaymentMethodId.equals(paymentMethodId), "Specified payment method id " + paymentMethodId + " doesn't match the one on the payment " + effectivePaymentMethodId); } else { // If the payment method is not specified, retrieve the default one on the account; it could still be null, in which case @@ -154,37 +152,37 @@ public UUID run(final boolean isApiPayment, final TransactionType transactionTyp final EnteringStateCallback enteringStateCallback; switch (transactionType) { case PURCHASE: - operationCallback = new PurchaseOperation(daoHelper, locker, paymentPluginDispatcher, paymentStateContext); + operationCallback = new PurchaseOperation(daoHelper, locker, paymentPluginDispatcher, paymentConfig, paymentStateContext); leavingStateCallback = new PurchaseInitiated(daoHelper, paymentStateContext); enteringStateCallback = new PurchaseCompleted(daoHelper, paymentStateContext); break; case AUTHORIZE: - operationCallback = new AuthorizeOperation(daoHelper, locker, paymentPluginDispatcher, paymentStateContext); + operationCallback = new AuthorizeOperation(daoHelper, locker, paymentPluginDispatcher, paymentConfig, paymentStateContext); leavingStateCallback = new AuthorizeInitiated(daoHelper, paymentStateContext); enteringStateCallback = new AuthorizeCompleted(daoHelper, paymentStateContext); break; case CAPTURE: - operationCallback = new CaptureOperation(daoHelper, locker, paymentPluginDispatcher, paymentStateContext); + operationCallback = new CaptureOperation(daoHelper, locker, paymentPluginDispatcher, paymentConfig, paymentStateContext); leavingStateCallback = new CaptureInitiated(daoHelper, paymentStateContext); enteringStateCallback = new CaptureCompleted(daoHelper, paymentStateContext); break; case VOID: - operationCallback = new VoidOperation(daoHelper, locker, paymentPluginDispatcher, paymentStateContext); + operationCallback = new VoidOperation(daoHelper, locker, paymentPluginDispatcher, paymentConfig, paymentStateContext); leavingStateCallback = new VoidInitiated(daoHelper, paymentStateContext); enteringStateCallback = new VoidCompleted(daoHelper, paymentStateContext); break; case REFUND: - operationCallback = new RefundOperation(daoHelper, locker, paymentPluginDispatcher, paymentStateContext); + operationCallback = new RefundOperation(daoHelper, locker, paymentPluginDispatcher, paymentConfig, paymentStateContext); leavingStateCallback = new RefundInitiated(daoHelper, paymentStateContext); enteringStateCallback = new RefundCompleted(daoHelper, paymentStateContext); break; case CREDIT: - operationCallback = new CreditOperation(daoHelper, locker, paymentPluginDispatcher, paymentStateContext); + operationCallback = new CreditOperation(daoHelper, locker, paymentPluginDispatcher, paymentConfig, paymentStateContext); leavingStateCallback = new CreditInitiated(daoHelper, paymentStateContext); enteringStateCallback = new CreditCompleted(daoHelper, paymentStateContext); break; case CHARGEBACK: - operationCallback = new ChargebackOperation(daoHelper, locker, paymentPluginDispatcher, paymentStateContext); + operationCallback = new ChargebackOperation(daoHelper, locker, paymentPluginDispatcher, paymentConfig, paymentStateContext); leavingStateCallback = new ChargebackInitiated(daoHelper, paymentStateContext); enteringStateCallback = new ChargebackCompleted(daoHelper, paymentStateContext); break; @@ -242,4 +240,13 @@ public boolean apply(final PluginProperty input) { return invoiceProperty == null || invoiceProperty.getValue() == null ? null : invoiceProperty.getValue().toString(); } + + private UUID retrievePaymentId(@Nullable final String paymentExternalKey, final InternalCallContext internalCallContext) { + if (paymentExternalKey == null) { + return null; + } + + final PaymentModelDao payment = paymentDao.getPaymentByExternalKey(paymentExternalKey, internalCallContext); + return payment == null ? null : payment.getId(); + } } diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java index d9c097c0c3..8975b2256e 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java @@ -37,31 +37,40 @@ public class PaymentStateContext { - // HACK - protected UUID paymentMethodId; - protected UUID attemptId; - - // Stateful objects created by the callbacks and passed to the other following callbacks in the automaton - protected List onLeavingStateExistingTransactions; - protected PaymentTransactionModelDao paymentTransactionModelDao; - protected PaymentTransactionInfoPlugin paymentTransactionInfoPlugin; - protected BigDecimal amount; - protected String paymentExternalKey; - protected String paymentTransactionExternalKey; - protected Currency currency; - protected Iterable properties; - protected boolean skipOperationForUnknownTransaction; - - // Can be updated later via paymentTransactionModelDao (e.g. for auth or purchase) - protected final UUID paymentId; - protected final UUID transactionId; - protected final Account account; - protected final TransactionType transactionType; - protected final boolean shouldLockAccountAndDispatch; - protected final InternalCallContext internalCallContext; - protected final CallContext callContext; - protected final boolean isApiPayment; - protected final OperationResult overridePluginOperationResult; + + // The following fields (paymentId, transactionId, amount, currency) may take their value from the paymentTransactionModelDao *when they are not already set* + private PaymentTransactionModelDao paymentTransactionModelDao; + // Initialized in CTOR or only set through paymentTransactionModelDao + private UUID paymentId; + private UUID transactionId; + + // Can be overriden by control plugin + private BigDecimal amount; + private Currency currency; + private UUID paymentMethodId; + private Iterable properties; + + // Set in the doOperationCallback when coming back from payment plugin + private PaymentTransactionInfoPlugin paymentTransactionInfoPlugin; + + // Set in the control layer in the leavingState callback + private String paymentExternalKey; + private String paymentTransactionExternalKey; + + // Set in the control layer after creating the attempt in the enteringState callback + private UUID attemptId; + + // This is purely a performance improvement to avoid fetching the existing transactions for that payment throughout the state machine + private List onLeavingStateExistingTransactions; + + // Immutable + private final Account account; + private final TransactionType transactionType; + private final boolean shouldLockAccountAndDispatch; + private final OperationResult overridePluginOperationResult; + private final InternalCallContext internalCallContext; + private final CallContext callContext; + private final boolean isApiPayment; // Use to create new transactions only public PaymentStateContext(final boolean isApiPayment, @Nullable final UUID paymentId, @Nullable final String paymentTransactionExternalKey, final TransactionType transactionType, @@ -95,7 +104,6 @@ public PaymentStateContext(final boolean isApiPayment, @Nullable final UUID paym this.internalCallContext = internalCallContext; this.callContext = callContext; this.onLeavingStateExistingTransactions = ImmutableList.of(); - this.skipOperationForUnknownTransaction = false; } public boolean isApiPayment() { @@ -112,6 +120,18 @@ public PaymentTransactionModelDao getPaymentTransactionModelDao() { public void setPaymentTransactionModelDao(final PaymentTransactionModelDao paymentTransactionModelDao) { this.paymentTransactionModelDao = paymentTransactionModelDao; + if (paymentId == null) { + this.paymentId = paymentTransactionModelDao.getPaymentId(); + } + if (transactionId == null) { + this.transactionId = paymentTransactionModelDao.getId(); + } + if (amount == null) { + this.amount = paymentTransactionModelDao.getAmount(); + } + if (currency == null) { + this.currency = paymentTransactionModelDao.getCurrency(); + } } public List getOnLeavingStateExistingTransactions() { @@ -131,11 +151,11 @@ public void setPaymentTransactionInfoPlugin(final PaymentTransactionInfoPlugin p } public UUID getPaymentId() { - return paymentId != null ? paymentId : (paymentTransactionModelDao != null ? paymentTransactionModelDao.getPaymentId() : null); + return paymentId; } public UUID getTransactionId() { - return transactionId != null ? transactionId : (paymentTransactionModelDao != null ? paymentTransactionModelDao.getId() : null); + return transactionId; } public String getPaymentExternalKey() { @@ -202,11 +222,16 @@ public CallContext getCallContext() { return callContext; } - public boolean isSkipOperationForUnknownTransaction() { - return skipOperationForUnknownTransaction; + public void setAmount(final BigDecimal adjustedAmount) { + this.amount = adjustedAmount; + } + + public void setCurrency(final Currency currency) { + this.currency = currency; } - public void setSkipOperationForUnknownTransaction(final boolean skipOperationForUnknownTransaction) { - this.skipOperationForUnknownTransaction = skipOperationForUnknownTransaction; + public void setProperties(final Iterable properties) { + this.properties = properties; } + } diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java index d6a12c4576..db1e2fada4 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java @@ -58,6 +58,9 @@ public class PaymentStateMachineHelper { private static final String CHARGEBACK_SUCCESS = "CHARGEBACK_SUCCESS"; private static final String AUTHORIZE_PENDING = "AUTH_PENDING"; + private static final String PURCHASE_PENDING = "PURCHASE_PENDING"; + private static final String REFUND_PENDING = "REFUND_PENDING"; + private static final String CREDIT_PENDING = "CREDIT_PENDING"; private static final String AUTHORIZE_FAILED = "AUTH_FAILED"; private static final String CAPTURE_FAILED = "CAPTURE_FAILED"; @@ -116,8 +119,14 @@ public String getPendingStateForTransaction(final TransactionType transactionTyp switch (transactionType) { case AUTHORIZE: return AUTHORIZE_PENDING; + case PURCHASE: + return PURCHASE_PENDING; + case REFUND: + return REFUND_PENDING; + case CREDIT: + return CREDIT_PENDING; default: - throw new IllegalStateException("Unsupported transaction type " + transactionType); + throw new IllegalStateException("No PENDING state for transaction type " + transactionType); } } diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java index 9c1dbc70bc..dd4d222bbe 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java @@ -20,7 +20,6 @@ import java.math.BigDecimal; import java.util.List; import java.util.UUID; -import java.util.concurrent.ExecutorService; import javax.annotation.Nullable; import javax.inject.Inject; @@ -32,21 +31,23 @@ import org.killbill.automaton.State; import org.killbill.automaton.State.EnteringStateCallback; import org.killbill.automaton.State.LeavingStateCallback; -import org.killbill.automaton.StateMachineConfig; import org.killbill.billing.ErrorCode; import org.killbill.billing.account.api.Account; import org.killbill.billing.callcontext.InternalCallContext; import org.killbill.billing.catalog.api.Currency; +import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; import org.killbill.billing.osgi.api.OSGIServiceRegistration; import org.killbill.billing.payment.api.Payment; import org.killbill.billing.payment.api.PaymentApiException; import org.killbill.billing.payment.api.PluginProperty; import org.killbill.billing.payment.api.TransactionType; +import org.killbill.billing.payment.core.PaymentExecutors; import org.killbill.billing.payment.core.PaymentProcessor; import org.killbill.billing.payment.core.sm.control.AuthorizeControlOperation; import org.killbill.billing.payment.core.sm.control.CaptureControlOperation; import org.killbill.billing.payment.core.sm.control.ChargebackControlOperation; import org.killbill.billing.payment.core.sm.control.CompletionControlOperation; +import org.killbill.billing.payment.core.sm.control.ControlPluginRunner; import org.killbill.billing.payment.core.sm.control.CreditControlOperation; import org.killbill.billing.payment.core.sm.control.DefaultControlCompleted; import org.killbill.billing.payment.core.sm.control.DefaultControlInitiated; @@ -56,10 +57,8 @@ import org.killbill.billing.payment.core.sm.control.RefundControlOperation; import org.killbill.billing.payment.core.sm.control.VoidControlOperation; import org.killbill.billing.payment.dao.PaymentDao; -import org.killbill.billing.payment.glue.PaymentModule; import org.killbill.billing.payment.plugin.api.PaymentPluginApi; import org.killbill.billing.payment.retry.BaseRetryService.RetryServiceScheduler; -import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; import org.killbill.billing.util.callcontext.CallContext; import org.killbill.billing.util.config.PaymentConfig; import org.killbill.bus.api.PersistentBus; @@ -67,9 +66,9 @@ import org.killbill.commons.locker.GlobalLocker; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED; import static org.killbill.billing.payment.glue.PaymentModule.RETRYABLE_NAMED; public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner { @@ -78,16 +77,21 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner private final PaymentProcessor paymentProcessor; private final RetryServiceScheduler retryServiceScheduler; private final PaymentControlStateMachineHelper paymentControlStateMachineHelper; + private final ControlPluginRunner controlPluginRunner; + private final PaymentConfig paymentConfig; @Inject - public PluginControlPaymentAutomatonRunner(@Named(PaymentModule.STATE_MACHINE_PAYMENT) final StateMachineConfig stateMachineConfig, final PaymentDao paymentDao, final GlobalLocker locker, final OSGIServiceRegistration pluginRegistry, + public PluginControlPaymentAutomatonRunner(final PaymentDao paymentDao, final GlobalLocker locker, final OSGIServiceRegistration pluginRegistry, final OSGIServiceRegistration paymentControlPluginRegistry, final Clock clock, final PaymentProcessor paymentProcessor, @Named(RETRYABLE_NAMED) final RetryServiceScheduler retryServiceScheduler, - final PaymentConfig paymentConfig, @Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor, final PaymentStateMachineHelper paymentSMHelper, final PaymentControlStateMachineHelper paymentControlStateMachineHelper, final PersistentBus eventBus) { - super(stateMachineConfig, paymentConfig, paymentDao, locker, pluginRegistry, clock, executor, eventBus, paymentSMHelper); + final PaymentConfig paymentConfig, final PaymentExecutors executors, final PaymentStateMachineHelper paymentSMHelper, final PaymentControlStateMachineHelper paymentControlStateMachineHelper, + final ControlPluginRunner controlPluginRunner, final PersistentBus eventBus) { + super(paymentConfig, paymentDao, locker, pluginRegistry, clock, executors, eventBus, paymentSMHelper); this.paymentProcessor = paymentProcessor; this.paymentControlPluginRegistry = paymentControlPluginRegistry; this.retryServiceScheduler = retryServiceScheduler; this.paymentControlStateMachineHelper = paymentControlStateMachineHelper; + this.controlPluginRunner = controlPluginRunner; + this.paymentConfig = paymentConfig; } public Payment run(final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId, @@ -116,15 +120,14 @@ public Payment run(final State state, final boolean isApiPayment, final Transact state.runOperation(paymentControlStateMachineHelper.getOperation(), callback, enteringStateCallback, leavingStateCallback); } catch (final MissingEntryException e) { - throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), "")); + throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, MoreObjects.firstNonNull(e.getMessage(), "")); } catch (final OperationException e) { - if (e.getCause() == null) { - // Unclear if we should check whether there is a result that was set and return that result. - throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), "")); - } else if (e.getCause() instanceof PaymentApiException) { + if (e.getCause() instanceof PaymentApiException) { throw (PaymentApiException) e.getCause(); - } else { - throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), "")); + // If the result is set (and cause is null), that means we created a Payment but the associated transaction status is 'XXX_FAILURE', + // we don't throw, and return the failed Payment instead to be consistent with what happens when we don't go through control api. + } else if (e.getCause() != null || paymentStateContext.getResult() == null) { + throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, MoreObjects.firstNonNull(e.getMessage(), "")); } } return paymentStateContext.getResult(); @@ -132,7 +135,7 @@ public Payment run(final State state, final boolean isApiPayment, final Transact public Payment completeRun(final PaymentStateControlContext paymentStateContext) throws PaymentApiException { try { - final OperationCallback callback = new CompletionControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + final OperationCallback callback = new CompletionControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner); final LeavingStateCallback leavingStateCallback = new NoopControlInitiated(); final EnteringStateCallback enteringStateCallback = new DefaultControlCompleted(this, paymentStateContext, paymentControlStateMachineHelper.getRetriedState(), retryServiceScheduler); @@ -165,25 +168,25 @@ OperationCallback createOperationCallback(final TransactionType transactionType, final OperationCallback callback; switch (transactionType) { case AUTHORIZE: - callback = new AuthorizeControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + callback = new AuthorizeControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner); break; case CAPTURE: - callback = new CaptureControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + callback = new CaptureControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner); break; case PURCHASE: - callback = new PurchaseControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + callback = new PurchaseControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner); break; case VOID: - callback = new VoidControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + callback = new VoidControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner); break; case CREDIT: - callback = new CreditControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + callback = new CreditControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner); break; case REFUND: - callback = new RefundControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + callback = new RefundControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner); break; case CHARGEBACK: - callback = new ChargebackControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + callback = new ChargebackControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner); break; default: throw new IllegalStateException("Unsupported transaction type " + transactionType); diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/AuthorizeControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/AuthorizeControlOperation.java index 96344610b2..4f05f5e787 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/AuthorizeControlOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/AuthorizeControlOperation.java @@ -23,12 +23,18 @@ import org.killbill.billing.payment.core.PaymentProcessor; import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; public class AuthorizeControlOperation extends OperationControlCallback { - public AuthorizeControlOperation(final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration paymentControlPluginRegistry) { - super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + public AuthorizeControlOperation(final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, + final PaymentStateControlContext paymentStateContext, + final PaymentProcessor paymentProcessor, + final ControlPluginRunner controlPluginRunner) { + super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentConfig, controlPluginRunner); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CaptureControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CaptureControlOperation.java index 2d325c46fb..54f133f039 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CaptureControlOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CaptureControlOperation.java @@ -23,12 +23,18 @@ import org.killbill.billing.payment.core.PaymentProcessor; import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; public class CaptureControlOperation extends OperationControlCallback { - public CaptureControlOperation(final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration paymentControlPluginRegistry) { - super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + public CaptureControlOperation(final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, + final PaymentStateControlContext paymentStateContext, + final PaymentProcessor paymentProcessor, + final ControlPluginRunner controlPluginRunner) { + super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentConfig, controlPluginRunner); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ChargebackControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ChargebackControlOperation.java index 3f47fdcb74..4836a2bb86 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ChargebackControlOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ChargebackControlOperation.java @@ -24,12 +24,18 @@ import org.killbill.billing.payment.core.PaymentProcessor; import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; public class ChargebackControlOperation extends OperationControlCallback { - public ChargebackControlOperation(final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration paymentControlPluginRegistry) { - super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + public ChargebackControlOperation(final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, + final PaymentStateControlContext paymentStateContext, + final PaymentProcessor paymentProcessor, + final ControlPluginRunner controlPluginRunner) { + super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentConfig, controlPluginRunner); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java index d6369e783b..d7cdac926f 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java @@ -19,16 +19,17 @@ import org.killbill.automaton.OperationException; import org.killbill.automaton.OperationResult; -import org.killbill.billing.osgi.api.OSGIServiceRegistration; +import org.killbill.billing.control.plugin.api.PaymentApiType; import org.killbill.billing.payment.api.Payment; import org.killbill.billing.payment.api.PaymentApiException; import org.killbill.billing.payment.core.PaymentProcessor; -import org.killbill.billing.payment.core.ProcessorBase.WithAccountLockCallback; +import org.killbill.billing.payment.core.ProcessorBase.DispatcherCallback; +import org.killbill.billing.payment.core.sm.control.ControlPluginRunner.DefaultPaymentControlContext; import org.killbill.billing.payment.dao.PaymentTransactionModelDao; import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType; import org.killbill.billing.control.plugin.api.PaymentControlContext; -import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; // @@ -36,14 +37,19 @@ // public class CompletionControlOperation extends OperationControlCallback { - public CompletionControlOperation(final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration retryPluginRegistry) { - super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, retryPluginRegistry); + public CompletionControlOperation(final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, + final PaymentStateControlContext paymentStateContext, + final PaymentProcessor paymentProcessor, + final ControlPluginRunner controlPluginRunner) { + super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentConfig, controlPluginRunner); } @Override public OperationResult doOperationCallback() throws OperationException { - return dispatchWithAccountLockAndTimeout(new WithAccountLockCallback, OperationException>() { + return dispatchWithAccountLockAndTimeout(new DispatcherCallback, OperationException>() { @Override public PluginDispatcherReturnType doOperation() throws OperationException { final PaymentTransactionModelDao transaction = paymentStateContext.getPaymentTransactionModelDao(); @@ -54,7 +60,9 @@ public PluginDispatcherReturnType doOperation() throws Operatio paymentStateContext.getPaymentExternalKey(), transaction.getId(), paymentStateContext.getPaymentTransactionExternalKey(), + PaymentApiType.PAYMENT_TRANSACTION, paymentStateContext.getTransactionType(), + null, transaction.getAmount(), transaction.getCurrency(), transaction.getProcessedAmount(), diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java new file mode 100644 index 0000000000..9d037f9407 --- /dev/null +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java @@ -0,0 +1,380 @@ +/* + * 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.payment.core.sm.control; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import org.joda.time.DateTime; +import org.killbill.billing.account.api.Account; +import org.killbill.billing.callcontext.DefaultCallContext; +import org.killbill.billing.catalog.api.Currency; +import org.killbill.billing.control.plugin.api.HPPType; +import org.killbill.billing.control.plugin.api.OnFailurePaymentControlResult; +import org.killbill.billing.control.plugin.api.OnSuccessPaymentControlResult; +import org.killbill.billing.control.plugin.api.PaymentApiType; +import org.killbill.billing.control.plugin.api.PaymentControlApiException; +import org.killbill.billing.control.plugin.api.PaymentControlContext; +import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; +import org.killbill.billing.control.plugin.api.PriorPaymentControlResult; +import org.killbill.billing.osgi.api.OSGIServiceRegistration; +import org.killbill.billing.payment.api.PluginProperty; +import org.killbill.billing.payment.api.TransactionType; +import org.killbill.billing.payment.retry.DefaultFailureCallResult; +import org.killbill.billing.payment.retry.DefaultOnSuccessPaymentControlResult; +import org.killbill.billing.payment.retry.DefaultPriorPaymentControlResult; +import org.killbill.billing.util.callcontext.CallContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ControlPluginRunner { + + private static final Logger log = LoggerFactory.getLogger(ControlPluginRunner.class); + + private final OSGIServiceRegistration paymentControlPluginRegistry; + + @Inject + public ControlPluginRunner(final OSGIServiceRegistration paymentControlPluginRegistry) { + this.paymentControlPluginRegistry = paymentControlPluginRegistry; + } + + public PriorPaymentControlResult executePluginPriorCalls(final Account account, + final UUID paymentMethodId, + final UUID paymentAttemptId, + final UUID paymentId, + final String paymentExternalKey, + final String paymentTransactionExternalKey, + final PaymentApiType paymentApiType, + final TransactionType transactionType, + final HPPType hppType, + final BigDecimal amount, + final Currency currency, + final boolean isApiPayment, + final List paymentControlPluginNames, + final Iterable pluginProperties, + final CallContext callContext) throws PaymentControlApiException { + // Return as soon as the first plugin aborts, or the last result for the last plugin + PriorPaymentControlResult prevResult = new DefaultPriorPaymentControlResult(false, amount, currency, paymentMethodId, pluginProperties); + + // Those values are adjusted prior each call with the result of what previous call to plugin returned + Iterable inputPluginProperties = pluginProperties; + PaymentControlContext inputPaymentControlContext = new DefaultPaymentControlContext(account, + paymentMethodId, + paymentAttemptId, + paymentId, + paymentExternalKey, + paymentTransactionExternalKey, + paymentApiType, + transactionType, + hppType, + amount, + currency, + isApiPayment, + callContext); + + for (final String pluginName : paymentControlPluginNames) { + final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName); + if (plugin == null) { + // First call to plugin, we log warn, if plugin is not registered + log.warn("Skipping unknown payment control plugin {} when fetching results", pluginName); + continue; + } + prevResult = plugin.priorCall(inputPaymentControlContext, inputPluginProperties); + if (prevResult.getAdjustedPluginProperties() != null) { + inputPluginProperties = prevResult.getAdjustedPluginProperties(); + } + if (prevResult.isAborted()) { + break; + } + inputPaymentControlContext = new DefaultPaymentControlContext(account, + prevResult.getAdjustedPaymentMethodId() != null ? prevResult.getAdjustedPaymentMethodId() : paymentMethodId, + paymentAttemptId, + paymentId, + paymentExternalKey, + paymentTransactionExternalKey, + paymentApiType, + transactionType, + hppType, + prevResult.getAdjustedAmount() != null ? prevResult.getAdjustedAmount() : amount, + prevResult.getAdjustedCurrency() != null ? prevResult.getAdjustedCurrency() : currency, + isApiPayment, + callContext); + } + // Rebuild latest result to include inputPluginProperties + prevResult = new DefaultPriorPaymentControlResult(prevResult, inputPluginProperties); + return prevResult; + } + + public OnSuccessPaymentControlResult executePluginOnSuccessCalls(final Account account, + final UUID paymentMethodId, + final UUID paymentAttemptId, + final UUID paymentId, + final String paymentExternalKey, + final UUID transactionId, + final String paymentTransactionExternalKey, + final PaymentApiType paymentApiType, + final TransactionType transactionType, + final HPPType hppType, + final BigDecimal amount, + final Currency currency, + final BigDecimal processedAmount, + final Currency processedCurrency, + final boolean isApiPayment, + final List paymentControlPluginNames, + final Iterable pluginProperties, + final CallContext callContext) { + + final PaymentControlContext inputPaymentControlContext = new DefaultPaymentControlContext(account, + paymentMethodId, + paymentAttemptId, + paymentId, + paymentExternalKey, + transactionId, + paymentTransactionExternalKey, + paymentApiType, + transactionType, + hppType, + amount, + currency, + processedAmount, + processedCurrency, + isApiPayment, + callContext); + Iterable inputPluginProperties = pluginProperties; + for (final String pluginName : paymentControlPluginNames) { + final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName); + if (plugin != null) { + try { + final OnSuccessPaymentControlResult result = plugin.onSuccessCall(inputPaymentControlContext, inputPluginProperties); + if (result.getAdjustedPluginProperties() != null) { + inputPluginProperties = result.getAdjustedPluginProperties(); + } + // Exceptions from the control plugins are ignored (and logged) because the semantics on what to do are undefined. + } catch (final PaymentControlApiException e) { + log.warn("Plugin " + pluginName + " failed to complete executePluginOnSuccessCalls call for " + inputPaymentControlContext.getPaymentExternalKey(), e); + } catch (final RuntimeException e) { + log.warn("Plugin " + pluginName + " failed to complete executePluginOnSuccessCalls call for " + inputPaymentControlContext.getPaymentExternalKey(), e); + } + } + } + return new DefaultOnSuccessPaymentControlResult(inputPluginProperties); + } + + public OnFailurePaymentControlResult executePluginOnFailureCalls(final Account account, + final UUID paymentMethodId, + final UUID paymentAttemptId, + final UUID paymentId, + final String paymentExternalKey, + final String paymentTransactionExternalKey, + final PaymentApiType paymentApiType, + final TransactionType transactionType, + final HPPType hppType, + final BigDecimal amount, + final Currency currency, + final boolean isApiPayment, + final List paymentControlPluginNames, + final Iterable pluginProperties, + final CallContext callContext) { + + final PaymentControlContext inputPaymentControlContext = new DefaultPaymentControlContext(account, + paymentMethodId, + paymentAttemptId, + paymentId, + paymentExternalKey, + paymentTransactionExternalKey, + paymentApiType, + transactionType, + hppType, + amount, + currency, + isApiPayment, + callContext); + + DateTime candidate = null; + Iterable inputPluginProperties = pluginProperties; + + for (final String pluginName : paymentControlPluginNames) { + final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName); + if (plugin != null) { + try { + final OnFailurePaymentControlResult result = plugin.onFailureCall(inputPaymentControlContext, inputPluginProperties); + if (candidate == null) { + candidate = result.getNextRetryDate(); + } else if (result.getNextRetryDate() != null) { + candidate = candidate.compareTo(result.getNextRetryDate()) > 0 ? result.getNextRetryDate() : candidate; + } + + if (result.getAdjustedPluginProperties() != null) { + inputPluginProperties = result.getAdjustedPluginProperties(); + } + + } catch (final PaymentControlApiException e) { + log.warn("Plugin " + pluginName + " failed to return next retryDate for payment " + inputPaymentControlContext.getPaymentExternalKey(), e); + return new DefaultFailureCallResult(candidate, inputPluginProperties); + } + } + } + return new DefaultFailureCallResult(candidate, inputPluginProperties); + } + + public static class DefaultPaymentControlContext extends DefaultCallContext implements PaymentControlContext { + + private final Account account; + private final UUID paymentMethodId; + private final UUID attemptId; + private final UUID paymentId; + private final String paymentExternalKey; + private final UUID transactionId; + private final String transactionExternalKey; + private final PaymentApiType paymentApiType; + private final HPPType hppType; + private final TransactionType transactionType; + private final BigDecimal amount; + private final Currency currency; + private final BigDecimal processedAmount; + private final Currency processedCurrency; + private final boolean isApiPayment; + + public DefaultPaymentControlContext(final Account account, final UUID paymentMethodId, final UUID attemptId, @Nullable final UUID paymentId, final String paymentExternalKey, final String transactionExternalKey, + final PaymentApiType paymentApiType, final TransactionType transactionType, final HPPType hppType, final BigDecimal amount, final Currency currency, + final boolean isApiPayment, final CallContext callContext) { + this(account, paymentMethodId, attemptId, paymentId, paymentExternalKey, null, transactionExternalKey, paymentApiType, transactionType, hppType, amount, currency, null, null, isApiPayment, callContext); + } + + public DefaultPaymentControlContext(final Account account, final UUID paymentMethodId, final UUID attemptId, @Nullable final UUID paymentId, final String paymentExternalKey, @Nullable final UUID transactionId, final String transactionExternalKey, + final PaymentApiType paymentApiType, final TransactionType transactionType, final HPPType hppType, + final BigDecimal amount, final Currency currency, @Nullable final BigDecimal processedAmount, @Nullable final Currency processedCurrency, final boolean isApiPayment, final CallContext callContext) { + super(callContext.getTenantId(), callContext.getUserName(), callContext.getCallOrigin(), callContext.getUserType(), callContext.getReasonCode(), callContext.getComments(), callContext.getUserToken(), callContext.getCreatedDate(), callContext.getUpdatedDate()); + this.account = account; + this.paymentMethodId = paymentMethodId; + this.attemptId = attemptId; + this.paymentId = paymentId; + this.paymentExternalKey = paymentExternalKey; + this.transactionId = transactionId; + this.transactionExternalKey = transactionExternalKey; + this.paymentApiType = paymentApiType; + this.hppType = hppType; + this.transactionType = transactionType; + this.amount = amount; + this.currency = currency; + this.processedAmount = processedAmount; + this.processedCurrency = processedCurrency; + this.isApiPayment = isApiPayment; + } + + @Override + public UUID getAccountId() { + return account.getId(); + } + + @Override + public String getPaymentExternalKey() { + return paymentExternalKey; + } + + @Override + public String getTransactionExternalKey() { + return transactionExternalKey; + } + + @Override + public PaymentApiType getPaymentApiType() { + return paymentApiType; + } + + @Override + public TransactionType getTransactionType() { + return transactionType; + } + + @Override + public HPPType getHPPType() { + return hppType; + } + + @Override + public BigDecimal getAmount() { + return amount; + } + + @Override + public Currency getCurrency() { + return currency; + } + + @Override + public UUID getPaymentMethodId() { + return paymentMethodId; + } + + @Override + public UUID getPaymentId() { + return paymentId; + } + + @Override + public UUID getAttemptPaymentId() { + return attemptId; + } + + @Override + public BigDecimal getProcessedAmount() { + return processedAmount; + } + + @Override + public Currency getProcessedCurrency() { + return processedCurrency; + } + + @Override + public boolean isApiPayment() { + return isApiPayment; + } + + public UUID getTransactionId() { + return transactionId; + } + + @Override + public String toString() { + return "DefaultPaymentControlContext{" + + "account=" + account + + ", paymentMethodId=" + paymentMethodId + + ", attemptId=" + attemptId + + ", paymentId=" + paymentId + + ", paymentExternalKey='" + paymentExternalKey + '\'' + + ", transactionId=" + transactionId + + ", transactionExternalKey='" + transactionExternalKey + '\'' + + ", paymentApiType=" + paymentApiType + + ", hppType=" + hppType + + ", transactionType=" + transactionType + + ", amount=" + amount + + ", currency=" + currency + + ", processedAmount=" + processedAmount + + ", processedCurrency=" + processedCurrency + + ", isApiPayment=" + isApiPayment + + '}'; + } + } + +} diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CreditControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CreditControlOperation.java index d0db2109dc..5d2c6433d1 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CreditControlOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CreditControlOperation.java @@ -23,12 +23,18 @@ import org.killbill.billing.payment.core.PaymentProcessor; import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; public class CreditControlOperation extends OperationControlCallback { - public CreditControlOperation(final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration paymentControlPluginRegistry) { - super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + public CreditControlOperation(final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, + final PaymentStateControlContext paymentStateContext, + final PaymentProcessor paymentProcessor, + final ControlPluginRunner controlPluginRunner) { + super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentConfig, controlPluginRunner); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java index 54783cba7d..4ead401df3 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java @@ -79,7 +79,7 @@ public void leavingState(final State state) throws OperationException { // // We don't serialize any properties at this stage to avoid serializing sensitive information. // However, if after going through the control plugins, the attempt end up in RETRIED state, - // the properties will be serialized in the enteringState( callback (any plugin that sets a + // the properties will be serialized in the enteringState callback (any plugin that sets a // retried date is responsible to correctly remove sensitive information such as CVV, ...) // final byte[] serializedProperties = PluginPropertySerializer.serialize(ImmutableList.of()); diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java index e1c632fb04..025bef2491 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java @@ -19,7 +19,6 @@ import java.math.BigDecimal; import java.util.List; -import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; @@ -29,30 +28,25 @@ import org.killbill.automaton.Operation.OperationCallback; import org.killbill.automaton.OperationException; import org.killbill.automaton.OperationResult; -import org.killbill.billing.account.api.Account; -import org.killbill.billing.callcontext.DefaultCallContext; -import org.killbill.billing.catalog.api.Currency; import org.killbill.billing.control.plugin.api.OnFailurePaymentControlResult; import org.killbill.billing.control.plugin.api.OnSuccessPaymentControlResult; +import org.killbill.billing.control.plugin.api.PaymentApiType; import org.killbill.billing.control.plugin.api.PaymentControlApiException; import org.killbill.billing.control.plugin.api.PaymentControlContext; -import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; import org.killbill.billing.control.plugin.api.PriorPaymentControlResult; -import org.killbill.billing.osgi.api.OSGIServiceRegistration; import org.killbill.billing.payment.api.Payment; import org.killbill.billing.payment.api.PaymentApiException; import org.killbill.billing.payment.api.PaymentTransaction; import org.killbill.billing.payment.api.PluginProperty; import org.killbill.billing.payment.api.TransactionStatus; -import org.killbill.billing.payment.api.TransactionType; import org.killbill.billing.payment.core.PaymentProcessor; -import org.killbill.billing.payment.core.ProcessorBase.WithAccountLockCallback; +import org.killbill.billing.payment.core.ProcessorBase.DispatcherCallback; import org.killbill.billing.payment.core.sm.OperationCallbackBase; import org.killbill.billing.payment.core.sm.PaymentStateContext; +import org.killbill.billing.payment.core.sm.control.ControlPluginRunner.DefaultPaymentControlContext; import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType; -import org.killbill.billing.payment.retry.DefaultPriorPaymentControlResult; -import org.killbill.billing.util.callcontext.CallContext; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; import org.killbill.commons.locker.LockFailedException; import org.slf4j.Logger; @@ -62,15 +56,21 @@ public abstract class OperationControlCallback extends OperationCallbackBase implements OperationCallback { + private static final Logger logger = LoggerFactory.getLogger(OperationControlCallback.class); + protected final PaymentProcessor paymentProcessor; protected final PaymentStateControlContext paymentStateControlContext; - private final OSGIServiceRegistration paymentControlPluginRegistry; - private final Logger logger = LoggerFactory.getLogger(OperationControlCallback.class); - - protected OperationControlCallback(final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration retryPluginRegistry) { - super(locker, paymentPluginDispatcher, paymentStateContext); + private final ControlPluginRunner controlPluginRunner; + + protected OperationControlCallback(final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentStateControlContext paymentStateContext, + final PaymentProcessor paymentProcessor, + final PaymentConfig paymentConfig, + final ControlPluginRunner controlPluginRunner) { + super(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext); this.paymentProcessor = paymentProcessor; - this.paymentControlPluginRegistry = retryPluginRegistry; + this.controlPluginRunner = controlPluginRunner; this.paymentStateControlContext = paymentStateContext; } @@ -80,7 +80,7 @@ protected OperationControlCallback(final GlobalLocker locker, final PluginDispat @Override public OperationResult doOperationCallback() throws OperationException { - return dispatchWithAccountLockAndTimeout(new WithAccountLockCallback, OperationException>() { + return dispatchWithAccountLockAndTimeout(new DispatcherCallback, OperationException>() { @Override public PluginDispatcherReturnType doOperation() throws OperationException { @@ -91,7 +91,9 @@ public PluginDispatcherReturnType doOperation() throws Operatio paymentStateContext.getPaymentId(), paymentStateContext.getPaymentExternalKey(), paymentStateContext.getPaymentTransactionExternalKey(), + PaymentApiType.PAYMENT_TRANSACTION, paymentStateContext.getTransactionType(), + null, paymentStateContext.getAmount(), paymentStateContext.getCurrency(), paymentStateControlContext.isApiPayment(), @@ -116,33 +118,34 @@ public PluginDispatcherReturnType doOperation() throws Operatio final PaymentTransaction transaction = ((PaymentStateControlContext) paymentStateContext).getCurrentTransaction(); success = transaction.getTransactionStatus() == TransactionStatus.SUCCESS || transaction.getTransactionStatus() == TransactionStatus.PENDING; + final PaymentControlContext updatedPaymentControlContext = new DefaultPaymentControlContext(paymentStateContext.getAccount(), + paymentStateContext.getPaymentMethodId(), + paymentStateControlContext.getAttemptId(), + result.getId(), + result.getExternalKey(), + transaction.getId(), + paymentStateContext.getPaymentTransactionExternalKey(), + PaymentApiType.PAYMENT_TRANSACTION, + paymentStateContext.getTransactionType(), + null, + transaction.getAmount(), + transaction.getCurrency(), + transaction.getProcessedAmount(), + transaction.getProcessedCurrency(), + paymentStateControlContext.isApiPayment(), + paymentStateContext.getCallContext()); if (success) { - final PaymentControlContext updatedPaymentControlContext = new DefaultPaymentControlContext(paymentStateContext.getAccount(), - paymentStateContext.getPaymentMethodId(), - paymentStateControlContext.getAttemptId(), - result.getId(), - result.getExternalKey(), - transaction.getId(), - paymentStateContext.getPaymentTransactionExternalKey(), - paymentStateContext.getTransactionType(), - transaction.getAmount(), - transaction.getCurrency(), - transaction.getProcessedAmount(), - transaction.getProcessedCurrency(), - paymentStateControlContext.isApiPayment(), - paymentStateContext.getCallContext()); - executePluginOnSuccessCalls(paymentStateControlContext.getPaymentControlPluginNames(), updatedPaymentControlContext); return PluginDispatcher.createPluginDispatcherReturnType(OperationResult.SUCCESS); } else { - throw new OperationException(null, executePluginOnFailureCallsAndSetRetryDate(paymentStateControlContext, paymentControlContext)); + throw new OperationException(null, executePluginOnFailureCallsAndSetRetryDate(updatedPaymentControlContext)); } } catch (final PaymentApiException e) { // Wrap PaymentApiException, and throw a new OperationException with an ABORTED/FAILURE state based on the retry result. - throw new OperationException(e, executePluginOnFailureCallsAndSetRetryDate(paymentStateControlContext, paymentControlContext)); + throw new OperationException(e, executePluginOnFailureCallsAndSetRetryDate(paymentControlContext)); } catch (final RuntimeException e) { // Attempts to set the retry date in context if needed. - executePluginOnFailureCallsAndSetRetryDate(paymentStateControlContext, paymentControlContext); + executePluginOnFailureCallsAndSetRetryDate(paymentControlContext); throw e; } } @@ -177,70 +180,52 @@ private OperationResult getOperationResultOnException(final PaymentStateContext } private PriorPaymentControlResult executePluginPriorCalls(final List paymentControlPluginNames, final PaymentControlContext paymentControlContextArg) throws PaymentControlApiException { - // Return as soon as the first plugin aborts, or the last result for the last plugin - PriorPaymentControlResult prevResult = null; - // Those values are adjusted prior each call with the result of what previous call to plugin returned - PaymentControlContext inputPaymentControlContext = paymentControlContextArg; - Iterable inputPluginProperties = paymentStateContext.getProperties(); - - for (final String pluginName : paymentControlPluginNames) { - final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName); - if (plugin == null) { - // First call to plugin, we log warn, if plugin is not registered - logger.warn("Skipping unknown payment control plugin {} when fetching results", pluginName); - continue; - } - prevResult = plugin.priorCall(inputPaymentControlContext, inputPluginProperties); - if (prevResult.getAdjustedPluginProperties() != null) { - inputPluginProperties = prevResult.getAdjustedPluginProperties(); - } - if (prevResult.isAborted()) { - break; - } - inputPaymentControlContext = new DefaultPaymentControlContext(paymentStateContext.getAccount(), - prevResult.getAdjustedPaymentMethodId() != null ? prevResult.getAdjustedPaymentMethodId() : inputPaymentControlContext.getPaymentMethodId(), - paymentStateControlContext.getAttemptId(), - paymentStateContext.getPaymentId(), - paymentStateContext.getPaymentExternalKey(), - paymentStateContext.getPaymentTransactionExternalKey(), - paymentStateContext.getTransactionType(), - prevResult.getAdjustedAmount() != null ? prevResult.getAdjustedAmount() : inputPaymentControlContext.getAmount(), - prevResult.getAdjustedCurrency() != null ? prevResult.getAdjustedCurrency() : inputPaymentControlContext.getCurrency(), - paymentStateControlContext.isApiPayment(), - paymentStateContext.getCallContext()); - - } - // Rebuild latest result to include inputPluginProperties - prevResult = new DefaultPriorPaymentControlResult(prevResult, inputPluginProperties); - // Adjust context with all values if necessary - adjustStateContextForPriorCall(paymentStateContext, prevResult); - return prevResult; + final PriorPaymentControlResult result = controlPluginRunner.executePluginPriorCalls(paymentStateContext.getAccount(), + paymentControlContextArg.getPaymentMethodId(), + paymentStateControlContext.getAttemptId(), + paymentStateContext.getPaymentId(), + paymentStateContext.getPaymentExternalKey(), + paymentStateContext.getPaymentTransactionExternalKey(), + PaymentApiType.PAYMENT_TRANSACTION, + paymentStateContext.getTransactionType(), + null, + paymentControlContextArg.getAmount(), + paymentControlContextArg.getCurrency(), + paymentStateControlContext.isApiPayment(), + paymentControlPluginNames, + paymentStateContext.getProperties(), + paymentStateContext.getCallContext()); + + adjustStateContextForPriorCall(paymentStateContext, result); + return result; } protected void executePluginOnSuccessCalls(final List paymentControlPluginNames, final PaymentControlContext paymentControlContext) { - - Iterable inputPluginProperties = paymentStateContext.getProperties(); - for (final String pluginName : paymentControlPluginNames) { - final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName); - if (plugin != null) { - try { - final OnSuccessPaymentControlResult result = plugin.onSuccessCall(paymentControlContext, inputPluginProperties); - if (result.getAdjustedPluginProperties() != null) { - inputPluginProperties = result.getAdjustedPluginProperties(); - } - // Exceptions from the control plugins are ignored (and logged) because the semantics on what to do are undefined. - } catch (final PaymentControlApiException e) { - logger.warn("Plugin " + pluginName + " failed to complete executePluginOnSuccessCalls call for " + paymentControlContext.getPaymentExternalKey(), e); - } catch (final RuntimeException e) { - logger.warn("Plugin " + pluginName + " failed to complete executePluginOnSuccessCalls call for " + paymentControlContext.getPaymentExternalKey(), e); - } - } - } - adjustStateContextPluginProperties(paymentStateContext, inputPluginProperties); + // Values that were obtained/changed after the payment call was made (paymentId, processedAmount, processedCurrency,... needs to be extracted from the paymentControlContext) + // paymentId, paymentExternalKey, transactionAmount, transaction currency are extracted from paymentControlContext which was update from the operation result. + final OnSuccessPaymentControlResult result = controlPluginRunner.executePluginOnSuccessCalls(paymentStateContext.getAccount(), + paymentStateContext.getPaymentMethodId(), + paymentStateControlContext.getAttemptId(), + paymentControlContext.getPaymentId(), + paymentControlContext.getPaymentExternalKey(), + paymentControlContext.getTransactionId(), + paymentStateContext.getPaymentTransactionExternalKey(), + PaymentApiType.PAYMENT_TRANSACTION, + paymentStateContext.getTransactionType(), + null, + paymentControlContext.getAmount(), + paymentControlContext.getCurrency(), + paymentControlContext.getProcessedAmount(), + paymentControlContext.getProcessedCurrency(), + paymentStateControlContext.isApiPayment(), + paymentControlPluginNames, + paymentStateContext.getProperties(), + paymentStateContext.getCallContext()); + adjustStateContextPluginProperties(paymentStateContext, result.getAdjustedPluginProperties()); } - private OperationResult executePluginOnFailureCallsAndSetRetryDate(final PaymentStateControlContext paymentStateControlContext, final PaymentControlContext paymentControlContext) { + private OperationResult executePluginOnFailureCallsAndSetRetryDate(final PaymentControlContext paymentControlContext) { final DateTime retryDate = executePluginOnFailureCalls(paymentStateControlContext.getPaymentControlPluginNames(), paymentControlContext); if (retryDate != null) { ((PaymentStateControlContext) paymentStateContext).setRetryDate(retryDate); @@ -250,32 +235,23 @@ private OperationResult executePluginOnFailureCallsAndSetRetryDate(final Payment private DateTime executePluginOnFailureCalls(final List paymentControlPluginNames, final PaymentControlContext paymentControlContext) { - DateTime candidate = null; - Iterable inputPluginProperties = paymentStateContext.getProperties(); - - for (final String pluginName : paymentControlPluginNames) { - final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName); - if (plugin != null) { - try { - final OnFailurePaymentControlResult result = plugin.onFailureCall(paymentControlContext, inputPluginProperties); - if (candidate == null) { - candidate = result.getNextRetryDate(); - } else if (result.getNextRetryDate() != null) { - candidate = candidate.compareTo(result.getNextRetryDate()) > 0 ? result.getNextRetryDate() : candidate; - } - - if (result.getAdjustedPluginProperties() != null) { - inputPluginProperties = result.getAdjustedPluginProperties(); - } - - } catch (final PaymentControlApiException e) { - logger.warn("Plugin " + pluginName + " failed to return next retryDate for payment " + paymentControlContext.getPaymentExternalKey(), e); - return candidate; - } - } - } - adjustStateContextPluginProperties(paymentStateContext, inputPluginProperties); - return candidate; + final OnFailurePaymentControlResult result = controlPluginRunner.executePluginOnFailureCalls(paymentStateContext.getAccount(), + paymentControlContext.getPaymentMethodId(), + paymentStateControlContext.getAttemptId(), + paymentControlContext.getPaymentId(), + paymentControlContext.getPaymentExternalKey(), + paymentControlContext.getTransactionExternalKey(), + PaymentApiType.PAYMENT_TRANSACTION, + paymentControlContext.getTransactionType(), + null, + paymentControlContext.getAmount(), + paymentControlContext.getCurrency(), + paymentStateControlContext.isApiPayment(), + paymentControlPluginNames, + paymentStateContext.getProperties(), + paymentStateContext.getCallContext()); + adjustStateContextPluginProperties(paymentStateContext, result.getAdjustedPluginProperties()); + return result.getNextRetryDate(); } private void adjustStateContextForPriorCall(final PaymentStateContext inputContext, @Nullable final PriorPaymentControlResult pluginResult) { @@ -303,127 +279,4 @@ private void adjustStateContextPluginProperties(final PaymentStateContext inputC final PaymentStateControlContext input = (PaymentStateControlContext) inputContext; input.setProperties(pluginProperties); } - - public static class DefaultPaymentControlContext extends DefaultCallContext implements PaymentControlContext { - - private final Account account; - private final UUID paymentMethodId; - private final UUID attemptId; - private final UUID paymentId; - private final String paymentExternalKey; - private final UUID transactionId; - private final String transactionExternalKey; - private final TransactionType transactionType; - private final BigDecimal amount; - private final Currency currency; - private final BigDecimal processedAmount; - private final Currency processedCurrency; - private final boolean isApiPayment; - - public DefaultPaymentControlContext(final Account account, final UUID paymentMethodId, final UUID attemptId, @Nullable final UUID paymentId, final String paymentExternalKey, final String transactionExternalKey, final TransactionType transactionType, final BigDecimal amount, final Currency currency, - final boolean isApiPayment, final CallContext callContext) { - this(account, paymentMethodId, attemptId, paymentId, paymentExternalKey, null, transactionExternalKey, transactionType, amount, currency, null, null, isApiPayment, callContext); - } - - public DefaultPaymentControlContext(final Account account, final UUID paymentMethodId, final UUID attemptId, @Nullable final UUID paymentId, final String paymentExternalKey, @Nullable final UUID transactionId, final String transactionExternalKey, final TransactionType transactionType, - final BigDecimal amount, final Currency currency, @Nullable final BigDecimal processedAmount, @Nullable final Currency processedCurrency, final boolean isApiPayment, final CallContext callContext) { - super(callContext.getTenantId(), callContext.getUserName(), callContext.getCallOrigin(), callContext.getUserType(), callContext.getReasonCode(), callContext.getComments(), callContext.getUserToken(), callContext.getCreatedDate(), callContext.getUpdatedDate()); - this.account = account; - this.paymentMethodId = paymentMethodId; - this.attemptId = attemptId; - this.paymentId = paymentId; - this.paymentExternalKey = paymentExternalKey; - this.transactionId = transactionId; - this.transactionExternalKey = transactionExternalKey; - this.transactionType = transactionType; - this.amount = amount; - this.currency = currency; - this.processedAmount = processedAmount; - this.processedCurrency = processedCurrency; - this.isApiPayment = isApiPayment; - } - - @Override - public UUID getAccountId() { - return account.getId(); - } - - @Override - public String getPaymentExternalKey() { - return paymentExternalKey; - } - - @Override - public String getTransactionExternalKey() { - return transactionExternalKey; - } - - @Override - public TransactionType getTransactionType() { - return transactionType; - } - - @Override - public BigDecimal getAmount() { - return amount; - } - - @Override - public Currency getCurrency() { - return currency; - } - - @Override - public UUID getPaymentMethodId() { - return paymentMethodId; - } - - @Override - public UUID getPaymentId() { - return paymentId; - } - - @Override - public UUID getAttemptPaymentId() { - return attemptId; - } - - @Override - public BigDecimal getProcessedAmount() { - return processedAmount; - } - - @Override - public Currency getProcessedCurrency() { - return processedCurrency; - } - - @Override - public boolean isApiPayment() { - return isApiPayment; - } - - public UUID getTransactionId() { - return transactionId; - } - - @Override - public String toString() { - return "DefaultPaymentControlContext{" + - "account=" + account + - ", paymentMethodId=" + paymentMethodId + - ", attemptId=" + attemptId + - ", paymentId=" + paymentId + - ", paymentExternalKey='" + paymentExternalKey + '\'' + - ", transactionId=" + transactionId + - ", transactionExternalKey='" + transactionExternalKey + '\'' + - ", transactionType=" + transactionType + - ", amount=" + amount + - ", currency=" + currency + - ", processedAmount=" + processedAmount + - ", processedCurrency=" + processedCurrency + - ", isApiPayment=" + isApiPayment + - '}'; - } - } } diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java index d9af3a922b..8b51735552 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java @@ -72,17 +72,6 @@ public void setResult(final Payment result) { this.result = result; } - public void setAmount(final BigDecimal adjustedAmount) { - this.amount = adjustedAmount; - } - - public void setCurrency(final Currency currency) { - this.currency = currency; - } - - public void setProperties(final Iterable properties) { - this.properties = properties; - } public PaymentTransaction getCurrentTransaction() { if (result == null || result.getTransactions() == null) { @@ -91,7 +80,7 @@ public PaymentTransaction getCurrentTransaction() { return Iterables.tryFind(result.getTransactions(), new Predicate() { @Override public boolean apply(final PaymentTransaction input) { - return ((DefaultPaymentTransaction) input).getAttemptId().equals(attemptId); + return ((DefaultPaymentTransaction) input).getAttemptId().equals(getAttemptId()); } }).orNull(); } diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PurchaseControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PurchaseControlOperation.java index 602231e823..4a766c719a 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PurchaseControlOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PurchaseControlOperation.java @@ -23,12 +23,18 @@ import org.killbill.billing.payment.core.PaymentProcessor; import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; public class PurchaseControlOperation extends OperationControlCallback { - public PurchaseControlOperation(final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration retryPluginRegistry) { - super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, retryPluginRegistry); + public PurchaseControlOperation(final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, + final PaymentStateControlContext paymentStateContext, + final PaymentProcessor paymentProcessor, + final ControlPluginRunner controlPluginRunner) { + super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentConfig, controlPluginRunner); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/RefundControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/RefundControlOperation.java index d9152f09dd..4c60dd372f 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/RefundControlOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/RefundControlOperation.java @@ -23,12 +23,18 @@ import org.killbill.billing.payment.core.PaymentProcessor; import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; public class RefundControlOperation extends OperationControlCallback { - public RefundControlOperation(final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration paymentControlPluginRegistry) { - super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + public RefundControlOperation(final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, + final PaymentStateControlContext paymentStateContext, + final PaymentProcessor paymentProcessor, + final ControlPluginRunner controlPluginRunner) { + super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentConfig, controlPluginRunner); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/VoidControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/VoidControlOperation.java index 67f4d9b8a4..db74fa9fdb 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/VoidControlOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/VoidControlOperation.java @@ -23,12 +23,18 @@ import org.killbill.billing.payment.core.PaymentProcessor; import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; public class VoidControlOperation extends OperationControlCallback { - public VoidControlOperation(final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration paymentControlPluginRegistry) { - super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry); + public VoidControlOperation(final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, + final PaymentStateControlContext paymentStateContext, + final PaymentProcessor paymentProcessor, + final ControlPluginRunner controlPluginRunner) { + super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentConfig, controlPluginRunner); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/AuthorizeOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/AuthorizeOperation.java index 291908913a..d8eb4056a1 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/AuthorizeOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/AuthorizeOperation.java @@ -24,6 +24,7 @@ import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.payment.plugin.api.PaymentPluginApiException; import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,9 +34,11 @@ public class AuthorizeOperation extends PaymentOperation { private final Logger logger = LoggerFactory.getLogger(AuthorizeOperation.class); public AuthorizeOperation(final PaymentAutomatonDAOHelper daoHelper, - final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, + final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, final PaymentStateContext paymentStateContext) throws PaymentApiException { - super(locker, daoHelper, paymentPluginDispatcher, paymentStateContext); + super(locker, daoHelper, paymentPluginDispatcher, paymentConfig, paymentStateContext); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/CaptureOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/CaptureOperation.java index 43903b8339..e6b67c5bd1 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/CaptureOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/CaptureOperation.java @@ -24,6 +24,7 @@ import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.payment.plugin.api.PaymentPluginApiException; import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,9 +34,11 @@ public class CaptureOperation extends PaymentOperation { private final Logger logger = LoggerFactory.getLogger(CaptureOperation.class); public CaptureOperation(final PaymentAutomatonDAOHelper daoHelper, - final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, + final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, final PaymentStateContext paymentStateContext) throws PaymentApiException { - super(locker, daoHelper, paymentPluginDispatcher, paymentStateContext); + super(locker, daoHelper, paymentPluginDispatcher, paymentConfig, paymentStateContext); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackOperation.java index 0585c0fe49..a2993a68d0 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackOperation.java @@ -30,6 +30,7 @@ import org.killbill.billing.payment.plugin.api.PaymentPluginStatus; import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; import org.killbill.billing.payment.provider.DefaultNoOpPaymentInfoPlugin; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,9 +40,11 @@ public class ChargebackOperation extends PaymentOperation { private final Logger logger = LoggerFactory.getLogger(ChargebackOperation.class); public ChargebackOperation(final PaymentAutomatonDAOHelper daoHelper, - final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, + final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, final PaymentStateContext paymentStateContext) throws PaymentApiException { - super(locker, daoHelper, paymentPluginDispatcher, paymentStateContext); + super(locker, daoHelper, paymentPluginDispatcher, paymentConfig, paymentStateContext); } @Override @@ -77,6 +80,7 @@ protected PaymentTransactionInfoPlugin doCallSpecificOperationCallback() throws null, null, status, + null, null); } } diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/CreditOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/CreditOperation.java index c206625dd8..53b5634244 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/CreditOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/CreditOperation.java @@ -24,6 +24,7 @@ import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.payment.plugin.api.PaymentPluginApiException; import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,9 +34,11 @@ public class CreditOperation extends PaymentOperation { private final Logger logger = LoggerFactory.getLogger(CreditOperation.class); public CreditOperation(final PaymentAutomatonDAOHelper daoHelper, - final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, + final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, final PaymentStateContext paymentStateContext) throws PaymentApiException { - super(locker, daoHelper, paymentPluginDispatcher, paymentStateContext); + super(locker, daoHelper, paymentPluginDispatcher, paymentConfig, paymentStateContext); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentEnteringStateCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentEnteringStateCallback.java index 9860f6b828..f5dd2f2898 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentEnteringStateCallback.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentEnteringStateCallback.java @@ -17,8 +17,6 @@ package org.killbill.billing.payment.core.sm.payments; -import javax.annotation.Nullable; - import org.killbill.automaton.Operation; import org.killbill.automaton.OperationResult; import org.killbill.automaton.State; @@ -52,10 +50,6 @@ protected PaymentEnteringStateCallback(final PaymentAutomatonDAOHelper daoHelper public void enteringState(final State newState, final Operation.OperationCallback operationCallback, final OperationResult operationResult, final LeavingStateCallback leavingStateCallback) { logger.debug("Entering state {} with result {}", newState.getName(), operationResult); - if (paymentStateContext.isSkipOperationForUnknownTransaction()) { - return; - } - // If the transaction was not created -- for instance we had an exception in leavingState callback then we bail; if not, then update state: if (paymentStateContext.getPaymentTransactionModelDao() != null && paymentStateContext.getPaymentTransactionModelDao().getId() != null) { final PaymentTransactionInfoPlugin paymentInfoPlugin = paymentStateContext.getPaymentTransactionInfoPlugin(); diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java index 6fb17d72f8..edc6f707ed 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java @@ -71,21 +71,12 @@ public void leavingState(final State oldState) throws OperationException { existingPaymentTransactions = ImmutableList.of(); } + // Validate the payment transactions belong to the right payment + validatePaymentId(existingPaymentTransactions); + // Validate some constraints on the unicity of that paymentTransactionExternalKey validateUniqueTransactionExternalKey(existingPaymentTransactions); - // Handle UNKNOWN cases, where we skip the whole state machine and let the getPayment (through Janitor) logic refresh the state. - final PaymentTransactionModelDao unknownPaymentTransaction = getUnknownPaymentTransaction(existingPaymentTransactions); - if (unknownPaymentTransaction != null) { - // Reset the attemptId on the existing paymentTransaction row since it is not accurate - unknownPaymentTransaction.setAttemptId(paymentStateContext.getAttemptId()); - // Set the current paymentTransaction in the context (needed for the state machine logic) - paymentStateContext.setPaymentTransactionModelDao(unknownPaymentTransaction); - // Set special flag to bypass the state machine altogether (plugin will not be called, state will not be updated, no event will be sent unless state is fixed) - paymentStateContext.setSkipOperationForUnknownTransaction(true); - return; - } - // Handle PENDING cases, where we want to re-use the same transaction final PaymentTransactionModelDao pendingPaymentTransaction = getPendingPaymentTransaction(existingPaymentTransactions); if (pendingPaymentTransaction != null) { @@ -94,7 +85,7 @@ public void leavingState(final State oldState) throws OperationException { return; } - // At this point we are left with PAYMENT_FAILURE, PLUGIN_FAILURE or nothing, and we validated the uniquess of the paymentTransactionExternalKey so we will create a new row + // At this point we are left with PAYMENT_FAILURE, PLUGIN_FAILURE or nothing, and we validated the uniqueness of the paymentTransactionExternalKey so we will create a new row daoHelper.createNewPaymentTransaction(); } catch (PaymentApiException e) { @@ -141,4 +132,13 @@ public boolean apply(final PaymentTransactionModelDao input) { throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey()); } } + + // At this point, the payment id should have been populated for follow-up transactions (see PaymentAutomationRunner#run) + protected void validatePaymentId(final List existingPaymentTransactions) throws PaymentApiException { + for (final PaymentTransactionModelDao paymentTransactionModelDao : existingPaymentTransactions) { + if (!paymentTransactionModelDao.getPaymentId().equals(paymentStateContext.getPaymentId())) { + throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, paymentTransactionModelDao.getId(), "does not belong to payment " + paymentStateContext.getPaymentId()); + } + } + } } diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java index ba6e48dfac..c39facf70e 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java @@ -30,7 +30,7 @@ import org.killbill.billing.payment.api.TransactionStatus; import org.killbill.billing.payment.api.TransactionType; import org.killbill.billing.payment.core.PaymentTransactionInfoPluginConverter; -import org.killbill.billing.payment.core.ProcessorBase.WithAccountLockCallback; +import org.killbill.billing.payment.core.ProcessorBase.DispatcherCallback; import org.killbill.billing.payment.core.sm.OperationCallbackBase; import org.killbill.billing.payment.core.sm.PaymentAutomatonDAOHelper; import org.killbill.billing.payment.core.sm.PaymentStateContext; @@ -42,6 +42,7 @@ import org.killbill.billing.payment.plugin.api.PaymentPluginStatus; import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; import org.killbill.billing.payment.provider.DefaultNoOpPaymentInfoPlugin; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; import org.killbill.commons.locker.LockFailedException; @@ -59,18 +60,15 @@ public abstract class PaymentOperation extends OperationCallbackBase paymentPluginDispatcher, + final PaymentConfig paymentConfig, final PaymentStateContext paymentStateContext) { - super(locker, paymentPluginDispatcher, paymentStateContext); + super(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext); this.daoHelper = daoHelper; } @Override public OperationResult doOperationCallback() throws OperationException { - if (paymentStateContext.isSkipOperationForUnknownTransaction()) { - return OperationResult.SUCCESS; - } - try { this.plugin = daoHelper.getPaymentProviderPlugin(); @@ -123,6 +121,7 @@ private OperationException convertToUnknownTransactionStatusAndErroredPaymentSta paymentStateContext.getCallContext().getCreatedDate(), paymentStateContext.getCallContext().getCreatedDate(), PaymentPluginStatus.UNDEFINED, + null, null); paymentStateContext.setPaymentTransactionInfoPlugin(paymentInfoPlugin); return new OperationException(e, OperationResult.EXCEPTION); @@ -154,7 +153,7 @@ protected BigDecimal getSumAmount(final Iterable tra } private OperationResult doOperationCallbackWithDispatchAndAccountLock() throws OperationException { - return dispatchWithAccountLockAndTimeout(new WithAccountLockCallback, OperationException>() { + return dispatchWithAccountLockAndTimeout(new DispatcherCallback, OperationException>() { @Override public PluginDispatcherReturnType doOperation() throws OperationException { final OperationResult result = doSimpleOperationCallback(); @@ -201,6 +200,7 @@ private OperationResult doOperation() throws PaymentApiException { paymentStateContext.getPaymentTransactionModelDao().getEffectiveDate(), paymentStateContext.getPaymentTransactionModelDao().getCreatedDate(), buildPaymentPluginStatusFromOperationResult(paymentStateContext.getOverridePluginOperationResult()), + null, null); paymentStateContext.setPaymentTransactionInfoPlugin(paymentInfoPlugin); return paymentStateContext.getOverridePluginOperationResult(); diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PurchaseOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PurchaseOperation.java index 7c9aabbec5..1ddf4b1a92 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PurchaseOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PurchaseOperation.java @@ -24,6 +24,7 @@ import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.payment.plugin.api.PaymentPluginApiException; import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,9 +34,11 @@ public class PurchaseOperation extends PaymentOperation { private final Logger logger = LoggerFactory.getLogger(PurchaseOperation.class); public PurchaseOperation(final PaymentAutomatonDAOHelper daoHelper, - final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, + final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, final PaymentStateContext paymentStateContext) throws PaymentApiException { - super(locker, daoHelper, paymentPluginDispatcher, paymentStateContext); + super(locker, daoHelper, paymentPluginDispatcher, paymentConfig, paymentStateContext); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/RefundOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/RefundOperation.java index 77534c59a8..22d923988e 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/RefundOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/RefundOperation.java @@ -24,6 +24,7 @@ import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.payment.plugin.api.PaymentPluginApiException; import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,9 +34,11 @@ public class RefundOperation extends PaymentOperation { private final Logger logger = LoggerFactory.getLogger(RefundOperation.class); public RefundOperation(final PaymentAutomatonDAOHelper daoHelper, - final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, + final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, final PaymentStateContext paymentStateContext) throws PaymentApiException { - super(locker, daoHelper, paymentPluginDispatcher, paymentStateContext); + super(locker, daoHelper, paymentPluginDispatcher, paymentConfig, paymentStateContext); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/VoidOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/VoidOperation.java index a188a434bb..0d776b6e0a 100644 --- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/VoidOperation.java +++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/VoidOperation.java @@ -24,6 +24,7 @@ import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.payment.plugin.api.PaymentPluginApiException; import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,9 +34,11 @@ public class VoidOperation extends PaymentOperation { private final Logger logger = LoggerFactory.getLogger(VoidOperation.class); public VoidOperation(final PaymentAutomatonDAOHelper daoHelper, - final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, + final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, final PaymentStateContext paymentStateContext) throws PaymentApiException { - super(locker, daoHelper, paymentPluginDispatcher, paymentStateContext); + super(locker, daoHelper, paymentPluginDispatcher, paymentConfig, paymentStateContext); } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java b/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java new file mode 100644 index 0000000000..e78d8ade26 --- /dev/null +++ b/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java @@ -0,0 +1,44 @@ +/* + * 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.payment.dispatcher; + +import java.util.concurrent.Callable; + +import org.killbill.commons.request.Request; +import org.killbill.commons.request.RequestData; + +public class CallableWithRequestData implements Callable { + + private final RequestData requestData; + private final Callable delegate; + + public CallableWithRequestData(final RequestData requestData, final Callable delegate) { + this.requestData = requestData; + this.delegate = delegate; + } + + @Override + public T call() throws Exception { + try { + Request.setPerThreadRequestData(requestData); + return delegate.call(); + } finally { + Request.resetPerThreadRequestData(); + } + } +} diff --git a/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java b/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java index a60eff6f78..bda52e0e5f 100644 --- a/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java +++ b/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java @@ -20,24 +20,27 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.killbill.billing.payment.core.PaymentExecutors; import org.killbill.commons.profiling.Profiling; import org.killbill.commons.profiling.ProfilingData; +import org.killbill.commons.request.Request; public class PluginDispatcher { private final TimeUnit DEEFAULT_PLUGIN_TIMEOUT_UNIT = TimeUnit.SECONDS; private final long timeoutSeconds; - private final ExecutorService executor; + private final PaymentExecutors paymentExecutors; - public PluginDispatcher(final long timeoutSeconds, final ExecutorService executor) { + public PluginDispatcher(final long timeoutSeconds, final PaymentExecutors paymentExecutors) { this.timeoutSeconds = timeoutSeconds; - this.executor = executor; + this.paymentExecutors = paymentExecutors; } // TODO Once we switch fully to automata, should this throw PaymentPluginApiException instead? @@ -48,7 +51,12 @@ public ReturnType dispatchWithTimeout(final Callable> task, final long timeout, final TimeUnit unit) throws TimeoutException, ExecutionException, InterruptedException { - final Future> future = executor.submit(task); + final ExecutorService pluginExecutor = paymentExecutors.getPluginExecutorService(); + + // Wrap existing callable to keep the original requestId + final Callable> callableWithRequestData = new CallableWithRequestData(Request.getPerThreadRequestData(), task); + + final Future> future = pluginExecutor.submit(callableWithRequestData); final PluginDispatcherReturnType pluginDispatcherResult = future.get(timeout, unit); if (pluginDispatcherResult instanceof WithProfilingPluginDispatcherReturnType) { diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java index d003f743a0..93603b1d0f 100644 --- a/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java +++ b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java @@ -21,6 +21,7 @@ import org.killbill.billing.payment.api.PaymentApi; import org.killbill.billing.payment.api.PaymentService; import org.killbill.billing.payment.bus.PaymentBusEventHandler; +import org.killbill.billing.payment.core.PaymentExecutors; import org.killbill.billing.payment.invoice.PaymentTagHandler; import org.killbill.billing.payment.core.janitor.Janitor; import org.killbill.billing.payment.retry.DefaultRetryService; @@ -46,6 +47,7 @@ public class DefaultPaymentService implements PaymentService { private final PaymentApi api; private final DefaultRetryService retryService; private final Janitor janitor; + private final PaymentExecutors paymentExecutors; @Inject public DefaultPaymentService(final PaymentBusEventHandler paymentBusEventHandler, @@ -53,13 +55,15 @@ public DefaultPaymentService(final PaymentBusEventHandler paymentBusEventHandler final PaymentApi api, final DefaultRetryService retryService, final PersistentBus eventBus, - final Janitor janitor) { + final Janitor janitor, + final PaymentExecutors paymentExecutors) { this.paymentBusEventHandler = paymentBusEventHandler; this.tagHandler = tagHandler; this.eventBus = eventBus; this.api = api; this.retryService = retryService; this.janitor = janitor; + this.paymentExecutors = paymentExecutors; } @Override @@ -75,6 +79,7 @@ public void initialize() throws NotificationQueueAlreadyExists { } catch (final PersistentBus.EventBusException e) { log.error("Unable to register with the EventBus!", e); } + paymentExecutors.initialize(); retryService.initialize(); janitor.initialize(); } @@ -95,6 +100,12 @@ public void stop() throws NoSuchNotificationQueue { } retryService.stop(); janitor.stop(); + try { + paymentExecutors.stop(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("PaymentService got interrupted", e); + } } @Override diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java index fb3c06bf6c..c9a0c74eb6 100644 --- a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java +++ b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java @@ -18,16 +18,11 @@ package org.killbill.billing.payment.glue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; - import javax.inject.Provider; import org.killbill.automaton.DefaultStateMachineConfig; import org.killbill.automaton.StateMachineConfig; +import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; import org.killbill.billing.osgi.api.OSGIServiceRegistration; import org.killbill.billing.payment.api.AdminPaymentApi; import org.killbill.billing.payment.api.DefaultAdminPaymentApi; @@ -37,6 +32,7 @@ import org.killbill.billing.payment.api.PaymentGatewayApi; import org.killbill.billing.payment.api.PaymentService; import org.killbill.billing.payment.bus.PaymentBusEventHandler; +import org.killbill.billing.payment.core.PaymentExecutors; import org.killbill.billing.payment.core.PaymentGatewayProcessor; import org.killbill.billing.payment.core.PaymentMethodProcessor; import org.killbill.billing.payment.core.PaymentProcessor; @@ -47,6 +43,7 @@ import org.killbill.billing.payment.core.sm.PaymentControlStateMachineHelper; import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper; import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner; +import org.killbill.billing.payment.core.sm.control.ControlPluginRunner; import org.killbill.billing.payment.dao.DefaultPaymentDao; import org.killbill.billing.payment.dao.PaymentDao; import org.killbill.billing.payment.invoice.PaymentTagHandler; @@ -57,10 +54,8 @@ import org.killbill.billing.payment.retry.DefaultRetryService.DefaultRetryServiceScheduler; import org.killbill.billing.payment.retry.RetryService; import org.killbill.billing.platform.api.KillbillConfigSource; -import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; import org.killbill.billing.util.config.PaymentConfig; import org.killbill.billing.util.glue.KillBillModule; -import org.killbill.commons.concurrent.WithProfilingThreadPoolExecutor; import org.killbill.xmlloader.XMLLoader; import org.skife.config.ConfigurationObjectFactory; @@ -72,10 +67,7 @@ public class PaymentModule extends KillBillModule { - private static final String PLUGIN_THREAD_PREFIX = "Plugin-th-"; - public static final String JANITOR_EXECUTOR_NAMED = "JanitorExecutor"; - public static final String PLUGIN_EXECUTOR_NAMED = "PluginExecutor"; public static final String RETRYABLE_NAMED = "Retryable"; public static final String STATE_MACHINE_RETRY = "RetryStateMachine"; @@ -100,11 +92,6 @@ protected void installPaymentProviderPlugins(final PaymentConfig config) { } protected void installJanitor() { - final ScheduledExecutorService janitorExecutor = org.killbill.commons.concurrent.Executors.newSingleThreadScheduledExecutor("PaymentJanitor"); - bind(ScheduledExecutorService.class).annotatedWith(Names.named(JANITOR_EXECUTOR_NAMED)).toInstance(janitorExecutor); - - bind(IncompletePaymentTransactionTask.class).asEagerSingleton(); - bind(IncompletePaymentAttemptTask.class).asEagerSingleton(); bind(Janitor.class).asEagerSingleton(); } @@ -125,6 +112,8 @@ protected void installStateMachines() { bind(StateMachineProvider.class).annotatedWith(Names.named(STATE_MACHINE_PAYMENT)).toInstance(new StateMachineProvider(DEFAULT_STATE_MACHINE_PAYMENT_XML)); bind(StateMachineConfig.class).annotatedWith(Names.named(STATE_MACHINE_PAYMENT)).toProvider(Key.get(StateMachineProvider.class, Names.named(STATE_MACHINE_PAYMENT))); bind(PaymentStateMachineHelper.class).asEagerSingleton(); + + bind(ControlPluginRunner.class).asEagerSingleton(); } protected void installAutomatonRunner() { @@ -132,20 +121,6 @@ protected void installAutomatonRunner() { } protected void installProcessors(final PaymentConfig paymentConfig) { - - final ExecutorService pluginExecutorService = new WithProfilingThreadPoolExecutor(paymentConfig.getPaymentPluginThreadNb(), paymentConfig.getPaymentPluginThreadNb(), - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(), - new ThreadFactory() { - - @Override - public Thread newThread(final Runnable r) { - final Thread th = new Thread(r); - th.setName(PLUGIN_THREAD_PREFIX + th.getId()); - return th; - } - }); - bind(ExecutorService.class).annotatedWith(Names.named(PLUGIN_EXECUTOR_NAMED)).toInstance(pluginExecutorService); bind(PaymentProcessor.class).asEagerSingleton(); bind(PluginControlPaymentProcessor.class).asEagerSingleton(); bind(PaymentGatewayProcessor.class).asEagerSingleton(); @@ -167,6 +142,7 @@ protected void configure() { bind(PaymentBusEventHandler.class).asEagerSingleton(); bind(PaymentTagHandler.class).asEagerSingleton(); bind(PaymentService.class).to(DefaultPaymentService.class).asEagerSingleton(); + bind(PaymentExecutors.class).asEagerSingleton(); installPaymentProviderPlugins(paymentConfig); installPaymentDao(); installProcessors(paymentConfig); diff --git a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java index 0484897d87..51788f77bd 100644 --- a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java +++ b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java @@ -33,6 +33,7 @@ import org.killbill.billing.account.api.Account; import org.killbill.billing.callcontext.InternalCallContext; import org.killbill.billing.callcontext.InternalTenantContext; +import org.killbill.billing.control.plugin.api.PaymentApiType; import org.killbill.billing.invoice.api.Invoice; import org.killbill.billing.invoice.api.InvoiceApiException; import org.killbill.billing.invoice.api.InvoiceInternalApi; @@ -114,7 +115,9 @@ public InvoicePaymentControlPluginApi(final PaymentConfig paymentConfig, final I @Override public PriorPaymentControlResult priorCall(final PaymentControlContext paymentControlContext, final Iterable pluginProperties) throws PaymentControlApiException { + final TransactionType transactionType = paymentControlContext.getTransactionType(); + Preconditions.checkArgument(paymentControlContext.getPaymentApiType() == PaymentApiType.PAYMENT_TRANSACTION); Preconditions.checkArgument(transactionType == TransactionType.PURCHASE || transactionType == TransactionType.REFUND || transactionType == TransactionType.CHARGEBACK); @@ -146,7 +149,7 @@ public OnSuccessPaymentControlResult onSuccessCall(final PaymentControlContext p case PURCHASE: final UUID invoiceId = getInvoiceId(pluginProperties); existingInvoicePayment = invoiceApi.getInvoicePaymentForAttempt(paymentControlContext.getPaymentId(), internalContext); - if (existingInvoicePayment != null) { + if (existingInvoicePayment != null && existingInvoicePayment.isSuccess()) { log.info("onSuccessCall was already completed for payment purchase :" + paymentControlContext.getPaymentId()); } else { invoiceApi.notifyOfPayment(invoiceId, @@ -155,6 +158,7 @@ public OnSuccessPaymentControlResult onSuccessCall(final PaymentControlContext p paymentControlContext.getProcessedCurrency(), paymentControlContext.getPaymentId(), paymentControlContext.getCreatedDate(), + true, internalContext); } break; @@ -190,11 +194,28 @@ public OnSuccessPaymentControlResult onSuccessCall(final PaymentControlContext p } @Override - public OnFailurePaymentControlResult onFailureCall(final PaymentControlContext paymentControlContext, final Iterable properties) throws PaymentControlApiException { + public OnFailurePaymentControlResult onFailureCall(final PaymentControlContext paymentControlContext, final Iterable pluginProperties) throws PaymentControlApiException { final InternalCallContext internalContext = internalCallContextFactory.createInternalCallContext(paymentControlContext.getAccountId(), paymentControlContext); final TransactionType transactionType = paymentControlContext.getTransactionType(); switch (transactionType) { case PURCHASE: + final UUID invoiceId = getInvoiceId(pluginProperties); + if (paymentControlContext.getPaymentId() != null) { + try { + invoiceApi.notifyOfPayment(invoiceId, + paymentControlContext.getAmount(), + paymentControlContext.getCurrency(), + // processed currency may be null so we use currency; processed currency will be updated if/when payment succeeds + paymentControlContext.getCurrency(), + paymentControlContext.getPaymentId(), + paymentControlContext.getCreatedDate(), + false, + internalContext); + } catch (InvoiceApiException e) { + log.error("InvoicePaymentControlPluginApi onFailureCall failed ton update invoice for attemptId = " + paymentControlContext.getAttemptPaymentId() + ", transactionType = " + transactionType, e); + } + } + final DateTime nextRetryDate = computeNextRetryDate(paymentControlContext.getPaymentExternalKey(), paymentControlContext.isApiPayment(), internalContext); return new DefaultFailureCallResult(nextRetryDate); case REFUND: @@ -206,13 +227,13 @@ public OnFailurePaymentControlResult onFailureCall(final PaymentControlContext p } } - public void process_AUTO_PAY_OFF_removal(final Account account, final InternalCallContext internalCallContext) { - final List entries = controlDao.getAutoPayOffEntry(account.getId()); + public void process_AUTO_PAY_OFF_removal(final UUID accountId, final InternalCallContext internalCallContext) { + final List entries = controlDao.getAutoPayOffEntry(accountId); for (final PluginAutoPayOffModelDao cur : entries) { // TODO In theory we should pass not only PLUGIN_NAME, but also all the plugin list associated which the original call - retryServiceScheduler.scheduleRetry(ObjectType.ACCOUNT, account.getId(), cur.getAttemptId(), internalCallContext.getTenantRecordId(), ImmutableList.of(PLUGIN_NAME), clock.getUTCNow()); + retryServiceScheduler.scheduleRetry(ObjectType.ACCOUNT, accountId, cur.getAttemptId(), internalCallContext.getTenantRecordId(), ImmutableList.of(PLUGIN_NAME), clock.getUTCNow()); } - controlDao.removeAutoPayOffEntry(account.getId()); + controlDao.removeAutoPayOffEntry(accountId); } private UUID getInvoiceId(final Iterable pluginProperties) throws PaymentControlApiException { diff --git a/payment/src/main/java/org/killbill/billing/payment/invoice/PaymentTagHandler.java b/payment/src/main/java/org/killbill/billing/payment/invoice/PaymentTagHandler.java index 4113349f9b..6270c21d58 100644 --- a/payment/src/main/java/org/killbill/billing/payment/invoice/PaymentTagHandler.java +++ b/payment/src/main/java/org/killbill/billing/payment/invoice/PaymentTagHandler.java @@ -21,13 +21,11 @@ import java.util.UUID; 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.callcontext.InternalCallContext; +import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; import org.killbill.billing.events.ControlTagDeletionInternalEvent; import org.killbill.billing.osgi.api.OSGIServiceRegistration; -import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; import org.killbill.billing.util.callcontext.CallOrigin; import org.killbill.billing.util.callcontext.InternalCallContextFactory; import org.killbill.billing.util.callcontext.UserType; @@ -66,14 +64,9 @@ public void process_AUTO_PAY_OFF_removal(final ControlTagDeletionInternalEvent e } private void processUnpaid_AUTO_PAY_OFF_payments(final UUID accountId, final Long accountRecordId, final Long tenantRecordId, final UUID userToken) { - try { - final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, - "PaymentRequestProcessor", CallOrigin.INTERNAL, UserType.SYSTEM, userToken); - final Account account = accountApi.getAccountById(accountId, internalCallContext); - ((InvoicePaymentControlPluginApi) invoicePaymentControlPlugin).process_AUTO_PAY_OFF_removal(account, internalCallContext); + final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, + "PaymentRequestProcessor", CallOrigin.INTERNAL, UserType.SYSTEM, userToken); + ((InvoicePaymentControlPluginApi) invoicePaymentControlPlugin).process_AUTO_PAY_OFF_removal(accountId, internalCallContext); - } catch (final AccountApiException e) { - log.warn(String.format("Failed to process process removal AUTO_PAY_OFF for account %s", accountId), e); - } } } diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpGatewayNotification.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpGatewayNotification.java new file mode 100644 index 0000000000..e377fab9e0 --- /dev/null +++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpGatewayNotification.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015 Groupon, Inc + * Copyright 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.payment.provider; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.killbill.billing.payment.api.PluginProperty; +import org.killbill.billing.payment.plugin.api.GatewayNotification; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +public class DefaultNoOpGatewayNotification implements GatewayNotification { + + @Override + public UUID getKbPaymentId() { + return null; + } + + @Override + public int getStatus() { + return 200; + } + + @Override + public String getEntity() { + return null; + } + + @Override + public Map> getHeaders() { + return ImmutableMap.>of(); + } + + @Override + public List getProperties() { + return ImmutableList.of(); + } +} diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpHostedPaymentPageFormDescriptor.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpHostedPaymentPageFormDescriptor.java new file mode 100644 index 0000000000..f623aaf973 --- /dev/null +++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpHostedPaymentPageFormDescriptor.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015 Groupon, Inc + * Copyright 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.payment.provider; + +import java.util.List; +import java.util.UUID; + +import org.killbill.billing.payment.api.PluginProperty; +import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor; + +import com.google.common.collect.ImmutableList; + +public class DefaultNoOpHostedPaymentPageFormDescriptor implements HostedPaymentPageFormDescriptor { + + private final UUID kbAccountId; + + public DefaultNoOpHostedPaymentPageFormDescriptor(final UUID kbAccountId) { + this.kbAccountId = kbAccountId; + } + + @Override + public UUID getKbAccountId() { + return kbAccountId; + } + + @Override + public String getFormMethod() { + return null; + } + + @Override + public String getFormUrl() { + return null; + } + + @Override + public List getFormFields() { + return ImmutableList.of(); + } + + @Override + public List getProperties() { + return ImmutableList.of(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("DefaultNoOpHostedPaymentPageFormDescriptor{"); + sb.append("kbAccountId=").append(kbAccountId); + sb.append('}'); + return sb.toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final DefaultNoOpHostedPaymentPageFormDescriptor that = (DefaultNoOpHostedPaymentPageFormDescriptor) o; + + return !(kbAccountId != null ? !kbAccountId.equals(that.kbAccountId) : that.kbAccountId != null); + } + + @Override + public int hashCode() { + return kbAccountId != null ? kbAccountId.hashCode() : 0; + } +} diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java index e2b187d96a..2b3ade231f 100644 --- a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java +++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java @@ -37,12 +37,13 @@ public class DefaultNoOpPaymentInfoPlugin implements PaymentTransactionInfoPlugi private final DateTime effectiveDate; private final DateTime createdDate; private final PaymentPluginStatus status; - private final String error; + private final String gatewayError; + private final String gatewayErrorCode; private final Currency currency; private final TransactionType transactionType; public DefaultNoOpPaymentInfoPlugin(final UUID kbPaymentId, final UUID kbTransactionPaymentId, final TransactionType transactionType, final BigDecimal amount, final Currency currency, final DateTime effectiveDate, - final DateTime createdDate, final PaymentPluginStatus status, final String error) { + final DateTime createdDate, final PaymentPluginStatus status, final String gatewayErrorCode, final String gatewayError) { this.kbPaymentId = kbPaymentId; this.kbTransactionPaymentId = kbTransactionPaymentId; this.transactionType = transactionType; @@ -50,7 +51,8 @@ public DefaultNoOpPaymentInfoPlugin(final UUID kbPaymentId, final UUID kbTransac this.effectiveDate = effectiveDate; this.createdDate = createdDate; this.status = status; - this.error = error; + this.gatewayErrorCode = gatewayErrorCode; + this.gatewayError = gatewayError; this.currency = currency; } @@ -96,12 +98,12 @@ public DateTime getCreatedDate() { @Override public String getGatewayError() { - return error; + return gatewayError; } @Override public String getGatewayErrorCode() { - return null; + return gatewayErrorCode; } @Override @@ -127,7 +129,7 @@ public String toString() { sb.append(", effectiveDate=").append(effectiveDate); sb.append(", createdDate=").append(createdDate); sb.append(", status=").append(status); - sb.append(", error='").append(error).append('\''); + sb.append(", error='").append(gatewayError).append('\''); sb.append(", currency=").append(currency); sb.append('}'); return sb.toString(); @@ -156,7 +158,7 @@ public boolean equals(final Object o) { if (effectiveDate != null ? effectiveDate.compareTo(that.effectiveDate) != 0 : that.effectiveDate != null) { return false; } - if (error != null ? !error.equals(that.error) : that.error != null) { + if (gatewayError != null ? !gatewayError.equals(that.gatewayError) : that.gatewayError != null) { return false; } if (transactionType != null ? !transactionType.equals(that.transactionType) : that.transactionType != null) { @@ -184,7 +186,7 @@ public int hashCode() { result = 31 * result + (transactionType != null ? transactionType.hashCode() : 0); result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0); result = 31 * result + (status != null ? status.hashCode() : 0); - result = 31 * result + (error != null ? error.hashCode() : 0); + result = 31 * result + (gatewayError != null ? gatewayError.hashCode() : 0); result = 31 * result + (currency != null ? currency.hashCode() : 0); return result; } diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java deleted file mode 100644 index 2908799550..0000000000 --- a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * Copyright 2014 Groupon, Inc - * Copyright 2014 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.payment.provider; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.killbill.billing.catalog.api.Currency; -import org.killbill.billing.payment.api.PaymentMethodPlugin; -import org.killbill.billing.payment.api.PluginProperty; -import org.killbill.billing.payment.api.TransactionType; -import org.killbill.billing.payment.plugin.api.GatewayNotification; -import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor; -import org.killbill.billing.payment.plugin.api.NoOpPaymentPluginApi; -import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin; -import org.killbill.billing.payment.plugin.api.PaymentPluginApiException; -import org.killbill.billing.payment.plugin.api.PaymentPluginStatus; -import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; -import org.killbill.billing.util.callcontext.CallContext; -import org.killbill.billing.util.callcontext.TenantContext; -import org.killbill.billing.util.entity.DefaultPagination; -import org.killbill.billing.util.entity.Pagination; -import org.killbill.clock.Clock; - -import com.google.common.base.Predicate; -import com.google.common.collect.Collections2; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import com.google.inject.Inject; - -public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi { - - private static final String PLUGIN_NAME = "__NO_OP__"; - - private final AtomicBoolean makeNextInvoiceFailWithError = new AtomicBoolean(false); - private final AtomicBoolean makeNextInvoiceFailWithException = new AtomicBoolean(false); - private final AtomicBoolean makeAllInvoicesFailWithError = new AtomicBoolean(false); - - private final Map> payments = new ConcurrentHashMap>(); - private final Map> paymentMethods = new ConcurrentHashMap>(); - - private final Clock clock; - - @Inject - public DefaultNoOpPaymentProviderPlugin(final Clock clock) { - this.clock = clock; - clear(); - } - - @Override - public void clear() { - makeNextInvoiceFailWithException.set(false); - makeAllInvoicesFailWithError.set(false); - makeNextInvoiceFailWithError.set(false); - } - - @Override - public void makeNextPaymentFailWithError() { - makeNextInvoiceFailWithError.set(true); - } - - @Override - public void makeNextPaymentFailWithException() { - makeNextInvoiceFailWithException.set(true); - } - - @Override - public void makeAllInvoicesFailWithError(final boolean failure) { - makeAllInvoicesFailWithError.set(failure); - } - - @Override - public PaymentTransactionInfoPlugin authorizePayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable properties, final CallContext context) - throws PaymentPluginApiException { - return getInternalNoopPaymentInfoResult(kbPaymentId, kbTransactionId, TransactionType.AUTHORIZE, amount, currency); - } - - @Override - public PaymentTransactionInfoPlugin capturePayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable properties, final CallContext context) - throws PaymentPluginApiException { - return getInternalNoopPaymentInfoResult(kbPaymentId, kbTransactionId, TransactionType.CAPTURE, amount, currency); - } - - @Override - public PaymentTransactionInfoPlugin purchasePayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable properties, final CallContext context) throws PaymentPluginApiException { - return getInternalNoopPaymentInfoResult(kbPaymentId, kbTransactionId, TransactionType.PURCHASE, amount, currency); - } - - @Override - public PaymentTransactionInfoPlugin voidPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final Iterable properties, final CallContext context) - throws PaymentPluginApiException { - return getInternalNoopPaymentInfoResult(kbPaymentId, kbTransactionId, TransactionType.VOID, BigDecimal.ZERO, null); - } - - @Override - public PaymentTransactionInfoPlugin creditPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable properties, final CallContext context) - throws PaymentPluginApiException { - return getInternalNoopPaymentInfoResult(kbPaymentId, kbTransactionId, TransactionType.CREDIT, amount, currency); - } - - @Override - public List getPaymentInfo(final UUID kbAccountId, final UUID kbPaymentId, final Iterable properties, final TenantContext context) throws PaymentPluginApiException { - return payments.get(kbPaymentId.toString()); - } - - @Override - public Pagination searchPayments(final String searchKey, final Long offset, final Long limit, final Iterable properties, final TenantContext tenantContext) throws PaymentPluginApiException { - - final List flattenedTransactions = ImmutableList.copyOf(Iterables.concat(payments.values())); - final Collection filteredTransactions = Collections2.filter(flattenedTransactions, - new Predicate() { - @Override - public boolean apply(final PaymentTransactionInfoPlugin input) { - return (input.getKbPaymentId() != null && input.getKbPaymentId().toString().equals(searchKey)) || - (input.getFirstPaymentReferenceId() != null && input.getFirstPaymentReferenceId().contains(searchKey)) || - (input.getSecondPaymentReferenceId() != null && input.getSecondPaymentReferenceId().contains(searchKey)); - } - } - ); - - final ImmutableList allResults = ImmutableList.copyOf(filteredTransactions); - - final List results; - if (offset >= allResults.size()) { - results = ImmutableList.of(); - } else if (offset + limit > allResults.size()) { - results = allResults.subList(offset.intValue(), allResults.size()); - } else { - results = allResults.subList(offset.intValue(), offset.intValue() + limit.intValue()); - } - - return new DefaultPagination(offset, limit, (long) results.size(), (long) payments.values().size(), results.iterator()); - } - - @Override - public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final PaymentMethodPlugin paymentMethodProps, final boolean setDefault, final Iterable properties, final CallContext context) throws PaymentPluginApiException { - final PaymentMethodPlugin realWithID = new DefaultNoOpPaymentMethodPlugin(kbPaymentMethodId, paymentMethodProps); - List pms = paymentMethods.get(kbPaymentMethodId.toString()); - if (pms == null) { - pms = new LinkedList(); - paymentMethods.put(kbPaymentMethodId.toString(), pms); - } - pms.add(realWithID); - } - - @Override - public void deletePaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final Iterable properties, final CallContext context) throws PaymentPluginApiException { - PaymentMethodPlugin toBeDeleted = null; - final List pms = paymentMethods.get(kbPaymentMethodId.toString()); - if (pms != null) { - for (final PaymentMethodPlugin cur : pms) { - if (cur.getExternalPaymentMethodId().equals(kbPaymentMethodId.toString())) { - toBeDeleted = cur; - break; - } - } - } - - if (toBeDeleted != null) { - pms.remove(toBeDeleted); - } - } - - @Override - public PaymentMethodPlugin getPaymentMethodDetail(final UUID kbAccountId, final UUID kbPaymentMethodId, final Iterable properties, final TenantContext context) throws PaymentPluginApiException { - final List paymentMethodPlugins = paymentMethods.get(kbPaymentMethodId.toString()); - if (paymentMethodPlugins == null || paymentMethodPlugins.isEmpty()) { - return null; - } else { - return paymentMethodPlugins.get(0); - } - } - - @Override - public void setDefaultPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final Iterable properties, final CallContext context) throws PaymentPluginApiException { - } - - @Override - public List getPaymentMethods(final UUID kbAccountId, final boolean refreshFromGateway, final Iterable properties, final CallContext context) { - return ImmutableList.of(); - } - - @Override - public Pagination searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final Iterable properties, final TenantContext tenantContext) throws PaymentPluginApiException { - final ImmutableList allResults = ImmutableList.copyOf(Iterables.filter(Iterables.concat(paymentMethods.values()), new Predicate() { - @Override - public boolean apply(final PaymentMethodPlugin input) { - return input.getKbPaymentMethodId().toString().equals(searchKey); - } - })); - - final List results; - if (offset >= allResults.size()) { - results = ImmutableList.of(); - } else if (offset + limit > allResults.size()) { - results = allResults.subList(offset.intValue(), allResults.size()); - } else { - results = allResults.subList(offset.intValue(), offset.intValue() + limit.intValue()); - } - - return new DefaultPagination(offset, limit, (long) results.size(), (long) paymentMethods.values().size(), results.iterator()); - } - - @Override - public void resetPaymentMethods(final UUID kbAccountId, final List paymentMethods, final Iterable properties, final CallContext callContext) { - } - - @Override - public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final Iterable customFields, final Iterable properties, final CallContext callContext) { - return null; - } - - @Override - public GatewayNotification processNotification(final String notification, final Iterable properties, final CallContext callContext) throws PaymentPluginApiException { - return null; - } - - @Override - public PaymentTransactionInfoPlugin refundPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal refundAmount, final Currency currency, final Iterable properties, final CallContext context) throws PaymentPluginApiException { - final List transactions = getPaymentInfo(kbAccountId, kbPaymentId, properties, context); - if (transactions == null || transactions.size() == 0) { - throw new PaymentPluginApiException("", String.format("No payment found for payment id %s (plugin %s)", kbPaymentId.toString(), PLUGIN_NAME)); - } - - final Iterable refundTransactions = Iterables.filter(transactions, new Predicate() { - @Override - public boolean apply(final PaymentTransactionInfoPlugin input) { - return input.getTransactionType() == TransactionType.REFUND; - } - }); - - BigDecimal maxAmountRefundable = BigDecimal.ZERO; - for (PaymentTransactionInfoPlugin cur : refundTransactions) { - maxAmountRefundable = maxAmountRefundable.add(cur.getAmount()); - } - - if (maxAmountRefundable.compareTo(refundAmount) < 0) { - throw new PaymentPluginApiException("", String.format("Refund amount of %s for payment id %s is bigger than the payment amount %s (plugin %s)", - refundAmount, kbPaymentId.toString(), maxAmountRefundable, PLUGIN_NAME)); - } - return getInternalNoopPaymentInfoResult(kbPaymentId, kbTransactionId, TransactionType.REFUND, refundAmount, currency); - } - - private PaymentTransactionInfoPlugin getInternalNoopPaymentInfoResult(final UUID kbPaymentId, final UUID kbTransactionId, final TransactionType transactionType, final BigDecimal amount, final Currency currency) throws PaymentPluginApiException { - if (makeNextInvoiceFailWithException.getAndSet(false)) { - throw new PaymentPluginApiException("", "test error"); - } - - final PaymentPluginStatus status = (makeAllInvoicesFailWithError.get() || makeNextInvoiceFailWithError.getAndSet(false)) ? PaymentPluginStatus.ERROR : PaymentPluginStatus.PROCESSED; - BigDecimal totalAmount = amount; - - List paymentTransactionInfoPlugins = payments.get(kbPaymentId.toString()); - if (paymentTransactionInfoPlugins == null) { - paymentTransactionInfoPlugins = new ArrayList(); - payments.put(kbPaymentId.toString(), paymentTransactionInfoPlugins); - } - final PaymentTransactionInfoPlugin result = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, transactionType, totalAmount, currency, clock.getUTCNow(), clock.getUTCNow(), status, null); - paymentTransactionInfoPlugins.add(result); - return result; - } -} diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/ExternalPaymentProviderPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/ExternalPaymentProviderPlugin.java index a6167a82e4..33f1e3026b 100644 --- a/payment/src/main/java/org/killbill/billing/payment/provider/ExternalPaymentProviderPlugin.java +++ b/payment/src/main/java/org/killbill/billing/payment/provider/ExternalPaymentProviderPlugin.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 @@ -29,11 +29,11 @@ import org.killbill.billing.payment.api.TransactionType; import org.killbill.billing.payment.plugin.api.GatewayNotification; import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor; -import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin; import org.killbill.billing.payment.plugin.api.PaymentPluginApi; import org.killbill.billing.payment.plugin.api.PaymentPluginApiException; import org.killbill.billing.payment.plugin.api.PaymentPluginStatus; +import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; import org.killbill.billing.util.callcontext.CallContext; import org.killbill.billing.util.callcontext.TenantContext; import org.killbill.billing.util.entity.DefaultPagination; @@ -41,14 +41,11 @@ import org.killbill.clock.Clock; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterators; +import com.google.common.collect.ImmutableSet; import com.google.inject.Inject; /** * Special plugin used to record external payments (i.e. payments not issued by Killbill), such as checks. - *

- * The implementation is very similar to the no-op plugin, which it extends. This can potentially be an issue - * if Killbill is processing a lot of external payments as they are all kept in memory. */ public class ExternalPaymentProviderPlugin implements PaymentPluginApi { @@ -63,43 +60,42 @@ public ExternalPaymentProviderPlugin(final Clock clock) { @Override public PaymentTransactionInfoPlugin authorizePayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable properties, final CallContext callContext) throws PaymentPluginApiException { - return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.AUTHORIZE, amount, currency, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null); + return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.AUTHORIZE, amount, currency, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null, null); } @Override public PaymentTransactionInfoPlugin capturePayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable properties, final CallContext callContext) throws PaymentPluginApiException { - return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.CAPTURE, amount, currency, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null); + return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.CAPTURE, amount, currency, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null, null); } @Override public PaymentTransactionInfoPlugin purchasePayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable properties, final CallContext context) throws PaymentPluginApiException { - return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.PURCHASE, amount, currency, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null); + return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.PURCHASE, amount, currency, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null, null); } @Override public PaymentTransactionInfoPlugin voidPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final Iterable properties, final CallContext callContext) throws PaymentPluginApiException { - return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.VOID, BigDecimal.ZERO, null, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null); + return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.VOID, BigDecimal.ZERO, null, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null, null); } @Override public PaymentTransactionInfoPlugin creditPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable properties, final CallContext callContext) throws PaymentPluginApiException { - return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.CREDIT, amount, currency, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null); + return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.CREDIT, amount, currency, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null, null); } @Override public List getPaymentInfo(final UUID kbAccountId, final UUID kbPaymentId, final Iterable properties, final TenantContext context) throws PaymentPluginApiException { - // TODO broken... return ImmutableList.of(); } @Override public Pagination searchPayments(final String searchKey, final Long offset, final Long limit, final Iterable properties, final TenantContext tenantContext) throws PaymentPluginApiException { - return new DefaultPagination(offset, limit, 0L, 0L, Iterators.emptyIterator()); + return new DefaultPagination(offset, limit, 0L, 0L, ImmutableSet.of().iterator()); } @Override public PaymentTransactionInfoPlugin refundPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal refundAmount, final Currency currency, final Iterable properties, final CallContext context) throws PaymentPluginApiException { - return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.REFUND, BigDecimal.ZERO, currency, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null); + return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.REFUND, refundAmount, currency, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null, null); } @Override @@ -126,7 +122,7 @@ public List getPaymentMethods(final UUID kbAccountId, f @Override public Pagination searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final Iterable properties, final TenantContext tenantContext) throws PaymentPluginApiException { - return new DefaultPagination(offset, limit, 0L, 0L, Iterators.emptyIterator()); + return new DefaultPagination(offset, limit, 0L, 0L, ImmutableSet.of().iterator()); } @Override @@ -135,11 +131,11 @@ public void resetPaymentMethods(final UUID kbAccountId, final List customFields, final Iterable properties, final CallContext callContext) { - return null; + return new DefaultNoOpHostedPaymentPageFormDescriptor(kbAccountId); } @Override public GatewayNotification processNotification(final String notification, final Iterable properties, final CallContext callContext) throws PaymentPluginApiException { - return null; + return new DefaultNoOpGatewayNotification(); } } diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginModule.java b/payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginModule.java deleted file mode 100644 index 9b640c058a..0000000000 --- a/payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginModule.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * Copyright 2014 Groupon, Inc - * Copyright 2014 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.payment.provider; - -import org.killbill.billing.platform.api.KillbillConfigSource; -import org.killbill.billing.util.glue.KillBillModule; - -import com.google.inject.name.Names; - -public class NoOpPaymentProviderPluginModule extends KillBillModule { - - private final String instanceName; - - public NoOpPaymentProviderPluginModule(final String instanceName, final KillbillConfigSource configSource) { - super(configSource); - this.instanceName = instanceName; - } - - @Override - protected void configure() { - bind(DefaultNoOpPaymentProviderPlugin.class) - .annotatedWith(Names.named(instanceName)) - .toProvider(new NoOpPaymentProviderPluginProvider(instanceName)) - .asEagerSingleton(); - } -} diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginProvider.java b/payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginProvider.java deleted file mode 100644 index 8b687c01f6..0000000000 --- a/payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginProvider.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.payment.provider; - -import com.google.inject.Inject; -import com.google.inject.Provider; - -import org.killbill.billing.osgi.api.OSGIServiceDescriptor; -import org.killbill.billing.osgi.api.OSGIServiceRegistration; -import org.killbill.billing.payment.plugin.api.PaymentPluginApi; -import org.killbill.clock.Clock; - -public class NoOpPaymentProviderPluginProvider implements Provider { - - private final String instanceName; - - private Clock clock; - private OSGIServiceRegistration registry; - - public NoOpPaymentProviderPluginProvider(final String instanceName) { - this.instanceName = instanceName; - - } - - @Inject - public void setPaymentProviderPluginRegistry(final OSGIServiceRegistration registry, final Clock clock) { - this.clock = clock; - this.registry = registry; - } - - @Override - public DefaultNoOpPaymentProviderPlugin get() { - - final DefaultNoOpPaymentProviderPlugin plugin = new DefaultNoOpPaymentProviderPlugin(clock); - final OSGIServiceDescriptor desc = new OSGIServiceDescriptor() { - @Override - public String getPluginSymbolicName() { - return null; - } - @Override - public String getRegistrationName() { - return instanceName; - } - }; - registry.registerService(desc, plugin); - return plugin; - } -} diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultFailureCallResult.java b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultFailureCallResult.java index 91408b7ae3..d1bb59dac5 100644 --- a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultFailureCallResult.java +++ b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultFailureCallResult.java @@ -22,10 +22,20 @@ public class DefaultFailureCallResult implements OnFailurePaymentControlResult { + private final Iterable adjustedPluginProperties; private final DateTime nextRetryDate; + public DefaultFailureCallResult() { + this(null, null); + } + public DefaultFailureCallResult(final DateTime nextRetryDate) { + this(nextRetryDate, null); + } + + public DefaultFailureCallResult(final DateTime nextRetryDate, final Iterable adjustedPluginProperties) { this.nextRetryDate = nextRetryDate; + this.adjustedPluginProperties = adjustedPluginProperties; } @Override @@ -35,6 +45,6 @@ public DateTime getNextRetryDate() { @Override public Iterable getAdjustedPluginProperties() { - return null; + return adjustedPluginProperties; } } diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultOnSuccessPaymentControlResult.java b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultOnSuccessPaymentControlResult.java index 006003688f..a97dc584e2 100644 --- a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultOnSuccessPaymentControlResult.java +++ b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultOnSuccessPaymentControlResult.java @@ -22,8 +22,18 @@ public class DefaultOnSuccessPaymentControlResult implements OnSuccessPaymentControlResult { + private final Iterable adjustedPluginProperties; + + public DefaultOnSuccessPaymentControlResult() { + this(null); + } + + public DefaultOnSuccessPaymentControlResult(final Iterable adjustedPluginProperties) { + this.adjustedPluginProperties = adjustedPluginProperties; + } + @Override public Iterable getAdjustedPluginProperties() { - return null; + return adjustedPluginProperties; } } diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java index 778f2797d8..9af2ee5695 100644 --- a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java +++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java @@ -23,6 +23,8 @@ import org.killbill.billing.invoice.api.InvoiceInternalApi; import org.killbill.billing.osgi.api.OSGIServiceRegistration; import org.killbill.billing.payment.api.PaymentApi; +import org.killbill.billing.payment.api.PaymentGatewayApi; +import org.killbill.billing.payment.core.PaymentExecutors; import org.killbill.billing.payment.core.PaymentMethodProcessor; import org.killbill.billing.payment.core.PaymentProcessor; import org.killbill.billing.payment.core.PluginControlPaymentProcessor; @@ -62,6 +64,8 @@ public abstract class PaymentTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB { @Inject protected PaymentApi paymentApi; @Inject + protected PaymentGatewayApi paymentGatewayApi; + @Inject protected AccountInternalApi accountInternalApi; @Inject protected TestPaymentHelper testHelper; @@ -79,6 +83,8 @@ public abstract class PaymentTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB { protected DefaultRetryService retryService; @Inject protected CacheControllerDispatcher cacheControllerDispatcher; + @Inject + protected PaymentExecutors paymentExecutors; @Override protected KillbillConfigSource getConfigSource() { @@ -97,11 +103,13 @@ protected void beforeClass() throws Exception { @BeforeMethod(groups = "fast") public void beforeMethod() throws Exception { eventBus.start(); + paymentExecutors.initialize(); Profiling.resetPerThreadProfilingData(); } @AfterMethod(groups = "fast") public void afterMethod() throws Exception { + paymentExecutors.stop(); eventBus.stop(); } } diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java index 81c48135ec..14072c315c 100644 --- a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java +++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java @@ -23,6 +23,8 @@ import org.killbill.billing.invoice.api.InvoiceInternalApi; import org.killbill.billing.osgi.api.OSGIServiceRegistration; import org.killbill.billing.payment.api.PaymentApi; +import org.killbill.billing.payment.api.PaymentGatewayApi; +import org.killbill.billing.payment.core.PaymentExecutors; import org.killbill.billing.payment.core.PaymentProcessor; import org.killbill.billing.payment.core.PaymentMethodProcessor; import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper; @@ -60,6 +62,8 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu @Inject protected PaymentApi paymentApi; @Inject + protected PaymentGatewayApi paymentGatewayApi; + @Inject protected AccountInternalApi accountApi; @Inject protected PaymentStateMachineHelper paymentSMHelper; @@ -67,6 +71,8 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu protected PaymentDao paymentDao; @Inject protected TestPaymentHelper testHelper; + @Inject + protected PaymentExecutors paymentExecutors; @Override protected KillbillConfigSource getConfigSource() { @@ -84,6 +90,7 @@ protected void beforeClass() throws Exception { @BeforeMethod(groups = "slow") public void beforeMethod() throws Exception { super.beforeMethod(); + paymentExecutors.initialize(); eventBus.start(); Profiling.resetPerThreadProfilingData(); clock.resetDeltaFromReality(); @@ -93,5 +100,6 @@ public void beforeMethod() throws Exception { @AfterMethod(groups = "slow") public void afterMethod() throws Exception { eventBus.stop(); + paymentExecutors.stop(); } } diff --git a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java index 7ed69f5416..d81caa8ab2 100644 --- a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java +++ b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java @@ -47,6 +47,10 @@ import org.killbill.billing.payment.dao.PaymentTransactionModelDao; import org.killbill.billing.payment.glue.DefaultPaymentService; import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi; +import org.killbill.billing.payment.plugin.api.PaymentPluginApiException; +import org.killbill.billing.payment.plugin.api.PaymentPluginStatus; +import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; +import org.killbill.billing.payment.provider.DefaultNoOpPaymentInfoPlugin; import org.killbill.billing.payment.provider.MockPaymentProviderPlugin; import org.killbill.billing.platform.api.KillbillConfigSource; import org.killbill.billing.util.callcontext.InternalCallContextFactory; @@ -55,6 +59,7 @@ import org.killbill.notificationq.api.NotificationEventWithMetadata; import org.killbill.notificationq.api.NotificationQueueService; import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue; +import org.skife.config.TimeSpan; import org.skife.jdbi.v2.Handle; import org.skife.jdbi.v2.tweak.HandleCallback; import org.testng.Assert; @@ -114,18 +119,17 @@ protected KillbillConfigSource getConfigSource() { protected void beforeClass() throws Exception { super.beforeClass(); mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(MockPaymentProviderPlugin.PLUGIN_NAME); - janitor.initialize(); - janitor.start(); } @AfterClass(groups = "slow") protected void afterClass() throws Exception { - janitor.stop(); } @BeforeMethod(groups = "slow") public void beforeMethod() throws Exception { super.beforeMethod(); + janitor.initialize(); + janitor.start(); eventBus.register(handler); testListener.reset(); eventBus.register(testListener); @@ -135,6 +139,7 @@ public void beforeMethod() throws Exception { @AfterMethod(groups = "slow") public void afterMethod() throws Exception { + janitor.stop(); eventBus.unregister(handler); eventBus.unregister(testListener); super.afterMethod(); @@ -393,6 +398,58 @@ public void testPendingEntries() throws PaymentApiException, EventBusException, Assert.assertEquals(updatedPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS); } + // The test will check that when a PENDING entry stays PENDING, we go through all our retries and evebtually give up (no infinite loop of retries) + @Test(groups = "slow") + public void testPendingEntriesThatDontMove() throws PaymentApiException, EventBusException, NoSuchNotificationQueue, PaymentPluginApiException, InterruptedException { + + final BigDecimal requestedAmount = BigDecimal.TEN; + final String paymentExternalKey = "haha"; + final String transactionExternalKey = "hoho!"; + + testListener.pushExpectedEvent(NextEvent.PAYMENT); + final Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, account.getCurrency(), paymentExternalKey, + transactionExternalKey, ImmutableList.of(), callContext); + testListener.assertListenerStatus(); + + final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext); + + // Artificially move the transaction status to PENDING AND update state on the plugin as well + final List paymentTransactions = mockPaymentProviderPlugin.getPaymentInfo(account.getId(), payment.getId(), ImmutableList.of(), callContext); + final PaymentTransactionInfoPlugin oTx = paymentTransactions.remove(0); + final PaymentTransactionInfoPlugin updatePaymentTransaction = new DefaultNoOpPaymentInfoPlugin(oTx.getKbPaymentId(), oTx.getKbTransactionPaymentId(), oTx.getTransactionType(), oTx.getAmount(), oTx.getCurrency(), oTx.getCreatedDate(), oTx.getCreatedDate(), PaymentPluginStatus.PENDING, null, null); + paymentTransactions.add(updatePaymentTransaction); + mockPaymentProviderPlugin.updatePaymentTransactions(payment.getId(), paymentTransactions); + + final String paymentStateName = paymentSMHelper.getPendingStateForTransaction(TransactionType.AUTHORIZE).toString(); + + + testListener.pushExpectedEvent(NextEvent.PAYMENT); + paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName, + payment.getTransactions().get(0).getId(), TransactionStatus.PENDING, requestedAmount, account.getCurrency(), + "loup", "chat", internalCallContext); + testListener.assertListenerStatus(); + + // 15s,1m,3m,1h,1d,1d,1d,1d,1d + for (TimeSpan cur : paymentConfig.getIncompleteTransactionsRetries()) { + // Verify there is a notification to retry updating the value + assertEquals(getPendingNotificationCnt(internalCallContext), 1); + + clock.addDeltaFromReality(cur.getMillis() + 1); + + // We add a sleep here to make sure the notification gets processed. Note that calling assertNotificationsCompleted would not work + // because there is a point in time where the notification queue is empty (showing notification was processed), but the processing of the notification + // will itself enter a new notification, and so the synchronization is difficult without writing *too much code*. + Thread.sleep(1000); + + //assertNotificationsCompleted(internalCallContext, 5); + final Payment updatedPayment = paymentApi.getPayment(payment.getId(), false, ImmutableList.of(), callContext); + Assert.assertEquals(updatedPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING); + } + + assertEquals(getPendingNotificationCnt(internalCallContext), 0); + } + + private List createPropertiesForInvoice(final Invoice invoice) { final List result = new ArrayList(); result.add(new PluginProperty(InvoicePaymentControlPluginApi.PROP_IPCD_INVOICE_ID, invoice.getId().toString(), false)); @@ -441,5 +498,15 @@ public Boolean call() throws Exception { fail("Test failed ", e); } } + + private int getPendingNotificationCnt(final InternalCallContext internalCallContext) { + try { + return notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, Janitor.QUEUE_NAME).getFutureOrInProcessingNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId()).size(); + } catch (final Exception e) { + fail("Test failed ", e); + } + // Not reached.. + return -1; + } } diff --git a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java index 33b1484f36..cbc23e577e 100644 --- a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java +++ b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java @@ -159,16 +159,10 @@ public void testFailedPaymentWithOneSuccessfulRetry() throws Exception { Currency.USD)); setPaymentFailure(FailureType.PAYMENT_FAILURE); - boolean failed = false; final String paymentExternalKey = UUID.randomUUID().toString(); final String transactionExternalKey = UUID.randomUUID().toString(); - try { - pluginControlPaymentProcessor.createPurchase(false, account, account.getPaymentMethodId(), null, amount, Currency.USD, paymentExternalKey, transactionExternalKey, - createPropertiesForInvoice(invoice), ImmutableList.of(InvoicePaymentControlPluginApi.PLUGIN_NAME), callContext, internalCallContext); - } catch (final PaymentApiException e) { - failed = true; - } - assertTrue(failed); + pluginControlPaymentProcessor.createPurchase(false, account, account.getPaymentMethodId(), null, amount, Currency.USD, paymentExternalKey, transactionExternalKey, + createPropertiesForInvoice(invoice), ImmutableList.of(InvoicePaymentControlPluginApi.PLUGIN_NAME), callContext, internalCallContext); Payment payment = getPaymentForExternalKey(paymentExternalKey); List attempts = paymentDao.getPaymentAttempts(paymentExternalKey, internalCallContext); @@ -242,16 +236,10 @@ public void testFailedPaymentWithLastRetrySuccess() throws Exception { Currency.USD)); setPaymentFailure(FailureType.PAYMENT_FAILURE); - boolean failed = false; final String paymentExternalKey = UUID.randomUUID().toString(); final String transactionExternalKey = UUID.randomUUID().toString(); - try { - pluginControlPaymentProcessor.createPurchase(false, account, account.getPaymentMethodId(), null, amount, Currency.USD, paymentExternalKey, transactionExternalKey, - createPropertiesForInvoice(invoice), ImmutableList.of(InvoicePaymentControlPluginApi.PLUGIN_NAME), callContext, internalCallContext); - } catch (final PaymentApiException e) { - failed = true; - } - assertTrue(failed); + pluginControlPaymentProcessor.createPurchase(false, account, account.getPaymentMethodId(), null, amount, Currency.USD, paymentExternalKey, transactionExternalKey, + createPropertiesForInvoice(invoice), ImmutableList.of(InvoicePaymentControlPluginApi.PLUGIN_NAME), callContext, internalCallContext); Payment payment = getPaymentForExternalKey(paymentExternalKey); List attempts = paymentDao.getPaymentAttempts(paymentExternalKey, internalCallContext); @@ -335,16 +323,10 @@ public void testAbortedPayment() throws Exception { Currency.USD)); setPaymentFailure(FailureType.PAYMENT_FAILURE); - boolean failed = false; final String paymentExternalKey = UUID.randomUUID().toString(); final String transactionExternalKey = UUID.randomUUID().toString(); - try { - pluginControlPaymentProcessor.createPurchase(false, account, account.getPaymentMethodId(), null, amount, Currency.USD, paymentExternalKey, transactionExternalKey, - createPropertiesForInvoice(invoice), ImmutableList.of(InvoicePaymentControlPluginApi.PLUGIN_NAME), callContext, internalCallContext); - } catch (final PaymentApiException e) { - failed = true; - } - assertTrue(failed); + pluginControlPaymentProcessor.createPurchase(false, account, account.getPaymentMethodId(), null, amount, Currency.USD, paymentExternalKey, transactionExternalKey, + createPropertiesForInvoice(invoice), ImmutableList.of(InvoicePaymentControlPluginApi.PLUGIN_NAME), callContext, internalCallContext); Payment payment = getPaymentForExternalKey(paymentExternalKey); List attempts = paymentDao.getPaymentAttempts(paymentExternalKey, internalCallContext); diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java index e4d7b09f04..f843590474 100644 --- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java +++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java @@ -24,10 +24,13 @@ import java.util.List; import java.util.UUID; +import javax.annotation.Nullable; + import org.joda.time.LocalDate; import org.killbill.billing.ErrorCode; import org.killbill.billing.account.api.Account; import org.killbill.billing.catalog.api.Currency; +import org.killbill.billing.control.plugin.api.PaymentControlApiException; import org.killbill.billing.invoice.api.Invoice; import org.killbill.billing.invoice.api.InvoiceApiException; import org.killbill.billing.invoice.api.InvoiceItem; @@ -36,9 +39,11 @@ import org.killbill.billing.payment.dao.PaymentAttemptModelDao; import org.killbill.billing.payment.dao.PaymentSqlDao; import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi; -import org.killbill.billing.control.plugin.api.PaymentControlApiException; +import org.killbill.billing.payment.plugin.api.PaymentPluginStatus; +import org.killbill.billing.payment.provider.MockPaymentProviderPlugin; import org.killbill.bus.api.PersistentBus.EventBusException; import org.testng.Assert; +import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -50,6 +55,8 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB { + private MockPaymentProviderPlugin mockPaymentProviderPlugin; + final PaymentOptions INVOICE_PAYMENT = new PaymentOptions() { @Override public boolean isExternalPayment() { @@ -64,9 +71,16 @@ public List getPaymentControlPluginNames() { private Account account; + @BeforeClass(groups = "slow") + protected void beforeClass() throws Exception { + super.beforeClass(); + mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(MockPaymentProviderPlugin.PLUGIN_NAME); + } + @BeforeMethod(groups = "slow") public void beforeMethod() throws Exception { super.beforeMethod(); + mockPaymentProviderPlugin.clear(); account = testHelper.createTestAccount("bobo@gmail.com", true); } @@ -104,6 +118,42 @@ public void testCreateSuccessPurchase() throws PaymentApiException { assertNull(payment.getTransactions().get(0).getGatewayErrorCode()); } + @Test(groups = "slow") + public void testCreateFailedPurchase() throws PaymentApiException { + + final BigDecimal requestedAmount = BigDecimal.TEN; + + final String paymentExternalKey = "ohhhh"; + final String transactionExternalKey = "naaahhh"; + + mockPaymentProviderPlugin.makeNextPaymentFailWithError(); + + final Payment payment = paymentApi.createPurchase(account, account.getPaymentMethodId(), null, requestedAmount, Currency.AED, paymentExternalKey, transactionExternalKey, + ImmutableList.of(), callContext); + + assertEquals(payment.getExternalKey(), paymentExternalKey); + assertEquals(payment.getPaymentMethodId(), account.getPaymentMethodId()); + assertEquals(payment.getAccountId(), account.getId()); + assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0); + assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0); + assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0); + assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0); + assertEquals(payment.getCurrency(), Currency.AED); + + assertEquals(payment.getTransactions().size(), 1); + assertEquals(payment.getTransactions().get(0).getExternalKey(), transactionExternalKey); + assertEquals(payment.getTransactions().get(0).getPaymentId(), payment.getId()); + assertEquals(payment.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0); + assertEquals(payment.getTransactions().get(0).getCurrency(), Currency.AED); + assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0); + assertEquals(payment.getTransactions().get(0).getProcessedCurrency(), Currency.AED); + + assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE); + assertEquals(payment.getTransactions().get(0).getTransactionType(), TransactionType.PURCHASE); + assertNull(payment.getTransactions().get(0).getGatewayErrorMsg()); + assertNull(payment.getTransactions().get(0).getGatewayErrorCode()); + } + @Test(groups = "slow") public void testCreateSuccessAuthCapture() throws PaymentApiException { @@ -237,7 +287,6 @@ public void testCreateSuccessPurchaseWithPaymentControl() throws PaymentApiExcep requestedAmount, new BigDecimal("1.0"), Currency.USD)); - final Payment payment = paymentApi.createPurchaseWithPaymentControl(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, paymentExternalKey, transactionExternalKey, createPropertiesForInvoice(invoice), INVOICE_PAYMENT, callContext); @@ -268,6 +317,65 @@ public void testCreateSuccessPurchaseWithPaymentControl() throws PaymentApiExcep assertEquals(attempts.size(), 1); } + + + @Test(groups = "slow") + public void testCreateFailedPurchaseWithPaymentControl() throws PaymentApiException, InvoiceApiException, EventBusException { + + final BigDecimal requestedAmount = BigDecimal.TEN; + final UUID subscriptionId = UUID.randomUUID(); + final UUID bundleId = UUID.randomUUID(); + final LocalDate now = clock.getUTCToday(); + + final Invoice invoice = testHelper.createTestInvoice(account, now, Currency.USD); + + final String paymentExternalKey = invoice.getId().toString(); + final String transactionExternalKey = "brrrrrr"; + + mockPaymentProviderPlugin.makeNextPaymentFailWithError(); + + invoice.addInvoiceItem(new MockRecurringInvoiceItem(invoice.getId(), account.getId(), + subscriptionId, + bundleId, + "test plan", "test phase", null, + now, + now.plusMonths(1), + requestedAmount, + new BigDecimal("1.0"), + Currency.USD)); + try { + paymentApi.createPurchaseWithPaymentControl(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, paymentExternalKey, transactionExternalKey, + createPropertiesForInvoice(invoice), INVOICE_PAYMENT, callContext); + } catch (final PaymentApiException expected) { + assertTrue(true); + } + + + final List accountPayments = paymentApi.getAccountPayments(account.getId(), false, ImmutableList.of(), callContext); + assertEquals(accountPayments.size(), 1); + final Payment payment = accountPayments.get(0); + assertEquals(payment.getExternalKey(), paymentExternalKey); + assertEquals(payment.getPaymentMethodId(), account.getPaymentMethodId()); + assertEquals(payment.getAccountId(), account.getId()); + assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0); + assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0); + assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0); + assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0); + assertEquals(payment.getCurrency(), Currency.USD); + + assertEquals(payment.getTransactions().size(), 1); + assertEquals(payment.getTransactions().get(0).getExternalKey(), transactionExternalKey); + assertEquals(payment.getTransactions().get(0).getPaymentId(), payment.getId()); + assertEquals(payment.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0); + assertEquals(payment.getTransactions().get(0).getCurrency(), Currency.USD); + assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0); // This is weird... + assertEquals(payment.getTransactions().get(0).getProcessedCurrency(), Currency.USD); + + assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE); + assertEquals(payment.getTransactions().get(0).getTransactionType(), TransactionType.PURCHASE); + } + + @Test(groups = "slow") public void testCreateAbortedPurchaseWithPaymentControl() throws InvoiceApiException, EventBusException { @@ -578,60 +686,280 @@ public void testInvalidTransitionAfterFailure() throws PaymentApiException { } } - @Test(groups = "slow") - public void testApiRetryWithUnknownPaymentTransaction() throws Exception { + @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/371") + public void testApiWithDuplicatePendingPaymentTransaction() throws Exception { final BigDecimal requestedAmount = BigDecimal.TEN; - final String paymentExternalKey = UUID.randomUUID().toString(); - final String paymentTransactionExternalKey = UUID.randomUUID().toString(); - - final Payment badPayment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, account.getCurrency(), - paymentExternalKey, paymentTransactionExternalKey, ImmutableList.of(), callContext); + for (final TransactionType transactionType : ImmutableList.of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) { + final String payment1ExternalKey = UUID.randomUUID().toString(); + final String payment1TransactionExternalKey = UUID.randomUUID().toString(); + final String payment2ExternalKey = UUID.randomUUID().toString(); + final String payment2TransactionExternalKey = UUID.randomUUID().toString(); + final String payment3TransactionExternalKey = UUID.randomUUID().toString(); + + final Payment pendingPayment1 = createPayment(transactionType, null, payment1ExternalKey, payment1TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING); + Assert.assertNotNull(pendingPayment1); + Assert.assertEquals(pendingPayment1.getExternalKey(), payment1ExternalKey); + Assert.assertEquals(pendingPayment1.getTransactions().size(), 1); + Assert.assertEquals(pendingPayment1.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(pendingPayment1.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(pendingPayment1.getTransactions().get(0).getCurrency(), account.getCurrency()); + Assert.assertEquals(pendingPayment1.getTransactions().get(0).getExternalKey(), payment1TransactionExternalKey); + Assert.assertEquals(pendingPayment1.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING); + + // Attempt to create a second transaction for the same payment, but with a different transaction external key + final Payment pendingPayment2 = createPayment(transactionType, null, payment1ExternalKey, payment2TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING); + Assert.assertNotNull(pendingPayment2); + Assert.assertEquals(pendingPayment2.getId(), pendingPayment1.getId()); + Assert.assertEquals(pendingPayment2.getExternalKey(), payment1ExternalKey); + Assert.assertEquals(pendingPayment2.getTransactions().size(), 2); + Assert.assertEquals(pendingPayment2.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(pendingPayment2.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(pendingPayment2.getTransactions().get(0).getCurrency(), account.getCurrency()); + Assert.assertEquals(pendingPayment2.getTransactions().get(0).getExternalKey(), payment1TransactionExternalKey); + Assert.assertEquals(pendingPayment2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING); + Assert.assertEquals(pendingPayment2.getTransactions().get(1).getAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(pendingPayment2.getTransactions().get(1).getProcessedAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(pendingPayment2.getTransactions().get(1).getCurrency(), account.getCurrency()); + Assert.assertEquals(pendingPayment2.getTransactions().get(1).getExternalKey(), payment2TransactionExternalKey); + Assert.assertEquals(pendingPayment2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING); + + try { + // Verify we cannot use the same transaction external key on a different payment if the payment id isn't specified + createPayment(transactionType, null, payment2ExternalKey, payment1TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING); + Assert.fail(); + } catch (final PaymentApiException e) { + Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode()); + } + + try { + // Verify we cannot use the same transaction external key on a different payment if the payment id isn't specified + createPayment(transactionType, null, payment2ExternalKey, payment2TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING); + Assert.fail(); + } catch (final PaymentApiException e) { + Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode()); + } + + // Attempt to create a second transaction for a different payment + final Payment pendingPayment3 = createPayment(transactionType, null, payment2ExternalKey, payment3TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING); + Assert.assertNotNull(pendingPayment3); + Assert.assertNotEquals(pendingPayment3.getId(), pendingPayment1.getId()); + Assert.assertEquals(pendingPayment3.getExternalKey(), payment2ExternalKey); + Assert.assertEquals(pendingPayment3.getTransactions().size(), 1); + Assert.assertEquals(pendingPayment3.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(pendingPayment3.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(pendingPayment3.getTransactions().get(0).getCurrency(), account.getCurrency()); + Assert.assertEquals(pendingPayment3.getTransactions().get(0).getExternalKey(), payment3TransactionExternalKey); + Assert.assertEquals(pendingPayment3.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING); + } + } - final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.AUTHORIZE).toString(); - paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), badPayment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName, - badPayment.getTransactions().get(0).getId(), TransactionStatus.UNKNOWN, requestedAmount, account.getCurrency(), - "eroor 64", "bad something happened", internalCallContext); + @Test(groups = "slow") + public void testApiWithPendingPaymentTransaction() throws Exception { + for (final TransactionType transactionType : ImmutableList.of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) { + testApiWithPendingPaymentTransaction(transactionType, BigDecimal.TEN, BigDecimal.TEN); + testApiWithPendingPaymentTransaction(transactionType, BigDecimal.TEN, BigDecimal.ONE); + // See https://github.com/killbill/killbill/issues/372 + testApiWithPendingPaymentTransaction(transactionType, BigDecimal.TEN, null); + } + } - final Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, account.getCurrency(), - paymentExternalKey, paymentTransactionExternalKey, ImmutableList.of(), callContext); + @Test(groups = "slow") + public void testApiWithPendingRefundPaymentTransaction() throws Exception { + final String paymentExternalKey = UUID.randomUUID().toString(); + final String paymentTransactionExternalKey = UUID.randomUUID().toString(); + final String refundTransactionExternalKey = UUID.randomUUID().toString(); + final BigDecimal requestedAmount = BigDecimal.TEN; + final BigDecimal refundAmount = BigDecimal.ONE; + final Iterable pendingPluginProperties = ImmutableList.of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, TransactionStatus.PENDING.toString(), false)); - Assert.assertEquals(payment.getId(), badPayment.getId()); - Assert.assertEquals(payment.getExternalKey(), paymentExternalKey); + final Payment payment = createPayment(TransactionType.PURCHASE, null, paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED); + Assert.assertNotNull(payment); Assert.assertEquals(payment.getExternalKey(), paymentExternalKey); - Assert.assertEquals(payment.getTransactions().size(), 1); - Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS); + Assert.assertEquals(payment.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(payment.getTransactions().get(0).getCurrency(), account.getCurrency()); Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey); + Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS); + + final Payment pendingRefund = paymentApi.createRefund(account, + payment.getId(), + requestedAmount, + account.getCurrency(), + refundTransactionExternalKey, + pendingPluginProperties, + callContext); + verifyRefund(pendingRefund, paymentExternalKey, paymentTransactionExternalKey, refundTransactionExternalKey, requestedAmount, requestedAmount, TransactionStatus.PENDING); + + // Test Janitor path (regression test for https://github.com/killbill/killbill/issues/363) + verifyPaymentViaGetPath(pendingRefund); + + // See https://github.com/killbill/killbill/issues/372 + final Payment pendingRefund2 = paymentApi.createRefund(account, + payment.getId(), + null, + null, + refundTransactionExternalKey, + pendingPluginProperties, + callContext); + verifyRefund(pendingRefund2, paymentExternalKey, paymentTransactionExternalKey, refundTransactionExternalKey, requestedAmount, requestedAmount, TransactionStatus.PENDING); + + verifyPaymentViaGetPath(pendingRefund2); + + // Note: we change the refund amount + final Payment pendingRefund3 = paymentApi.createRefund(account, + payment.getId(), + refundAmount, + account.getCurrency(), + refundTransactionExternalKey, + pendingPluginProperties, + callContext); + verifyRefund(pendingRefund3, paymentExternalKey, paymentTransactionExternalKey, refundTransactionExternalKey, requestedAmount, refundAmount, TransactionStatus.PENDING); + + verifyPaymentViaGetPath(pendingRefund3); + + // Pass null, we revert back to the original refund amount + final Payment pendingRefund4 = paymentApi.createRefund(account, + payment.getId(), + null, + null, + refundTransactionExternalKey, + ImmutableList.of(), + callContext); + verifyRefund(pendingRefund4, paymentExternalKey, paymentTransactionExternalKey, refundTransactionExternalKey, requestedAmount, requestedAmount, TransactionStatus.SUCCESS); + + verifyPaymentViaGetPath(pendingRefund4); } - // Example of a 3D secure payment for instance - @Test(groups = "slow") - public void testApiWithPendingPaymentTransaction() throws Exception { - final BigDecimal requestedAmount = BigDecimal.TEN; + private void verifyRefund(final Payment refund, final String paymentExternalKey, final String paymentTransactionExternalKey, final String refundTransactionExternalKey, final BigDecimal requestedAmount, final BigDecimal refundAmount, final TransactionStatus transactionStatus) { + Assert.assertEquals(refund.getExternalKey(), paymentExternalKey); + Assert.assertEquals(refund.getTransactions().size(), 2); + Assert.assertEquals(refund.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(refund.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(refund.getTransactions().get(0).getCurrency(), account.getCurrency()); + Assert.assertEquals(refund.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey); + Assert.assertEquals(refund.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS); + Assert.assertEquals(refund.getTransactions().get(1).getAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(refund.getTransactions().get(1).getProcessedAmount().compareTo(refundAmount), 0); + Assert.assertEquals(refund.getTransactions().get(1).getCurrency(), account.getCurrency()); + Assert.assertEquals(refund.getTransactions().get(1).getExternalKey(), refundTransactionExternalKey); + Assert.assertEquals(refund.getTransactions().get(1).getTransactionStatus(), transactionStatus); + } + private Payment testApiWithPendingPaymentTransaction(final TransactionType transactionType, final BigDecimal requestedAmount, @Nullable final BigDecimal pendingAmount) throws PaymentApiException { final String paymentExternalKey = UUID.randomUUID().toString(); final String paymentTransactionExternalKey = UUID.randomUUID().toString(); - final Payment pendingPayment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, account.getCurrency(), - paymentExternalKey, paymentTransactionExternalKey, ImmutableList.of(), callContext); - - final String paymentStateName = paymentSMHelper.getPendingStateForTransaction(TransactionType.AUTHORIZE).toString(); - paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), pendingPayment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName, - pendingPayment.getTransactions().get(0).getId(), TransactionStatus.PENDING, requestedAmount, account.getCurrency(), - null, null, internalCallContext); - - final Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), pendingPayment.getId(), requestedAmount, account.getCurrency(), - paymentExternalKey, paymentTransactionExternalKey, ImmutableList.of(), callContext); - - Assert.assertEquals(payment.getId(), pendingPayment.getId()); - Assert.assertEquals(payment.getExternalKey(), paymentExternalKey); - Assert.assertEquals(payment.getExternalKey(), paymentExternalKey); + final Payment pendingPayment = createPayment(transactionType, null, paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING); + Assert.assertNotNull(pendingPayment); + Assert.assertEquals(pendingPayment.getExternalKey(), paymentExternalKey); + Assert.assertEquals(pendingPayment.getTransactions().size(), 1); + Assert.assertEquals(pendingPayment.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(pendingPayment.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(pendingPayment.getTransactions().get(0).getCurrency(), account.getCurrency()); + Assert.assertEquals(pendingPayment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey); + Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING); + + // Test Janitor path (regression test for https://github.com/killbill/killbill/issues/363) + verifyPaymentViaGetPath(pendingPayment); + + final Payment pendingPayment2 = createPayment(transactionType, pendingPayment.getId(), paymentExternalKey, paymentTransactionExternalKey, pendingAmount, PaymentPluginStatus.PENDING); + Assert.assertNotNull(pendingPayment2); + Assert.assertEquals(pendingPayment2.getExternalKey(), paymentExternalKey); + Assert.assertEquals(pendingPayment2.getTransactions().size(), 1); + Assert.assertEquals(pendingPayment2.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(pendingPayment2.getTransactions().get(0).getProcessedAmount().compareTo(pendingAmount == null ? requestedAmount : pendingAmount), 0); + Assert.assertEquals(pendingPayment2.getTransactions().get(0).getCurrency(), account.getCurrency()); + Assert.assertEquals(pendingPayment2.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey); + Assert.assertEquals(pendingPayment2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING); + + verifyPaymentViaGetPath(pendingPayment2); + + final Payment completedPayment = createPayment(transactionType, pendingPayment.getId(), paymentExternalKey, paymentTransactionExternalKey, pendingAmount, PaymentPluginStatus.PROCESSED); + Assert.assertNotNull(completedPayment); + Assert.assertEquals(completedPayment.getExternalKey(), paymentExternalKey); + Assert.assertEquals(completedPayment.getTransactions().size(), 1); + Assert.assertEquals(completedPayment.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0); + Assert.assertEquals(completedPayment.getTransactions().get(0).getProcessedAmount().compareTo(pendingAmount == null ? requestedAmount : pendingAmount), 0); + Assert.assertEquals(completedPayment.getTransactions().get(0).getCurrency(), account.getCurrency()); + Assert.assertEquals(completedPayment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey); + Assert.assertEquals(completedPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS); + + verifyPaymentViaGetPath(completedPayment); + + return completedPayment; + } - Assert.assertEquals(payment.getTransactions().size(), 1); - Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS); - Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey); + private void verifyPaymentViaGetPath(final Payment payment) throws PaymentApiException { + // We can't use Assert.assertEquals because the updateDate may have been updated by the Janitor + final Payment refreshedPayment = paymentApi.getPayment(payment.getId(), true, ImmutableList.of(), callContext); + + Assert.assertEquals(refreshedPayment.getAccountId(), payment.getAccountId()); + + Assert.assertEquals(refreshedPayment.getTransactions().size(), payment.getTransactions().size()); + Assert.assertEquals(refreshedPayment.getExternalKey(), payment.getExternalKey()); + Assert.assertEquals(refreshedPayment.getPaymentMethodId(), payment.getPaymentMethodId()); + Assert.assertEquals(refreshedPayment.getAccountId(), payment.getAccountId()); + Assert.assertEquals(refreshedPayment.getAuthAmount().compareTo(payment.getAuthAmount()), 0); + Assert.assertEquals(refreshedPayment.getCapturedAmount().compareTo(payment.getCapturedAmount()), 0); + Assert.assertEquals(refreshedPayment.getPurchasedAmount().compareTo(payment.getPurchasedAmount()), 0); + Assert.assertEquals(refreshedPayment.getRefundedAmount().compareTo(payment.getRefundedAmount()), 0); + Assert.assertEquals(refreshedPayment.getCurrency(), payment.getCurrency()); + + for (int i = 0; i < refreshedPayment.getTransactions().size(); i++) { + final PaymentTransaction refreshedPaymentTransaction = refreshedPayment.getTransactions().get(i); + final PaymentTransaction paymentTransaction = payment.getTransactions().get(i); + Assert.assertEquals(refreshedPaymentTransaction.getAmount().compareTo(paymentTransaction.getAmount()), 0); + Assert.assertEquals(refreshedPaymentTransaction.getProcessedAmount().compareTo(paymentTransaction.getProcessedAmount()), 0); + Assert.assertEquals(refreshedPaymentTransaction.getCurrency(), paymentTransaction.getCurrency()); + Assert.assertEquals(refreshedPaymentTransaction.getExternalKey(), paymentTransaction.getExternalKey()); + Assert.assertEquals(refreshedPaymentTransaction.getTransactionStatus(), paymentTransaction.getTransactionStatus()); + } + } + private Payment createPayment(final TransactionType transactionType, + @Nullable final UUID paymentId, + @Nullable final String paymentExternalKey, + @Nullable final String paymentTransactionExternalKey, + @Nullable final BigDecimal amount, + final PaymentPluginStatus paymentPluginStatus) throws PaymentApiException { + final Iterable pluginProperties = ImmutableList.of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, paymentPluginStatus.toString(), false)); + switch (transactionType) { + case AUTHORIZE: + return paymentApi.createAuthorization(account, + account.getPaymentMethodId(), + paymentId, + amount, + amount == null ? null : account.getCurrency(), + paymentExternalKey, + paymentTransactionExternalKey, + pluginProperties, + callContext); + case PURCHASE: + return paymentApi.createPurchase(account, + account.getPaymentMethodId(), + paymentId, + amount, + amount == null ? null : account.getCurrency(), + paymentExternalKey, + paymentTransactionExternalKey, + pluginProperties, + callContext); + case CREDIT: + return paymentApi.createCredit(account, + account.getPaymentMethodId(), + paymentId, + amount, + amount == null ? null : account.getCurrency(), + paymentExternalKey, + paymentTransactionExternalKey, + pluginProperties, + callContext); + default: + Assert.fail(); + return null; + } } private List createPropertiesForInvoice(final Invoice invoice) { diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java index 5d528a11c3..34f146db8f 100644 --- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java +++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java @@ -46,7 +46,7 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB { @Inject - private OSGIServiceRegistration retryPluginRegistry; + private OSGIServiceRegistration controlPluginRegistry; private Account account; private UUID newPaymentMethodId; @@ -58,7 +58,7 @@ public void beforeMethod() throws Exception { final PaymentMethodPlugin paymentMethodInfo = new DefaultNoOpPaymentMethodPlugin(UUID.randomUUID().toString(), false, null); newPaymentMethodId = paymentApi.addPaymentMethod(account, paymentMethodInfo.getExternalPaymentMethodId(), MockPaymentProviderPlugin.PLUGIN_NAME, false, paymentMethodInfo, ImmutableList.of(), callContext); - retryPluginRegistry.registerService(new OSGIServiceDescriptor() { + controlPluginRegistry.registerService(new OSGIServiceDescriptor() { @Override public String getPluginSymbolicName() { return null; diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentGatewayApiWithPaymentControl.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentGatewayApiWithPaymentControl.java new file mode 100644 index 0000000000..fb29bff014 --- /dev/null +++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentGatewayApiWithPaymentControl.java @@ -0,0 +1,257 @@ +/* + * 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.payment.api; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.killbill.billing.account.api.Account; +import org.killbill.billing.control.plugin.api.OnFailurePaymentControlResult; +import org.killbill.billing.control.plugin.api.OnSuccessPaymentControlResult; +import org.killbill.billing.control.plugin.api.PaymentControlApiException; +import org.killbill.billing.control.plugin.api.PaymentControlContext; +import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; +import org.killbill.billing.control.plugin.api.PriorPaymentControlResult; +import org.killbill.billing.osgi.api.OSGIServiceDescriptor; +import org.killbill.billing.osgi.api.OSGIServiceRegistration; +import org.killbill.billing.payment.PaymentTestSuiteNoDB; +import org.killbill.billing.payment.retry.DefaultFailureCallResult; +import org.killbill.billing.payment.retry.DefaultOnSuccessPaymentControlResult; +import org.killbill.billing.payment.retry.DefaultPriorPaymentControlResult; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.inject.Inject; + +public class TestPaymentGatewayApiWithPaymentControl extends PaymentTestSuiteNoDB { + + @Inject + private OSGIServiceRegistration controlPluginRegistry; + + private Account account; + + private PaymentOptions paymentOptions; + + private TestPaymentGatewayApiControlPlugin plugin; + private TestPaymentGatewayApiValidationPlugin validationPlugin; + + @BeforeMethod(groups = "fast") + public void beforeMethod() throws Exception { + + super.beforeMethod(); + + account = testHelper.createTestAccount("arthur@gmail.com", true); + + paymentOptions = new PaymentOptions() { + @Override + public boolean isExternalPayment() { + return false; + } + + @Override + public List getPaymentControlPluginNames() { + return ImmutableList.of(TestPaymentGatewayApiControlPlugin.PLUGIN_NAME, TestPaymentGatewayApiValidationPlugin.VALIDATION_PLUGIN_NAME); + } + }; + + plugin = new TestPaymentGatewayApiControlPlugin(); + + controlPluginRegistry.registerService(new OSGIServiceDescriptor() { + @Override + public String getPluginSymbolicName() { + return null; + } + + @Override + public String getRegistrationName() { + return TestPaymentGatewayApiControlPlugin.PLUGIN_NAME; + } + }, plugin); + + validationPlugin = new TestPaymentGatewayApiValidationPlugin(); + controlPluginRegistry.registerService(new OSGIServiceDescriptor() { + @Override + public String getPluginSymbolicName() { + return null; + } + + @Override + public String getRegistrationName() { + return TestPaymentGatewayApiValidationPlugin.VALIDATION_PLUGIN_NAME; + } + }, validationPlugin); + + } + + @Test(groups = "fast") + public void testBuildFormDescriptorWithPaymentControl() throws PaymentApiException { + + final List initialProperties = new ArrayList(); + initialProperties.add(new PluginProperty("keyA", "valueA", true)); + initialProperties.add(new PluginProperty("keyB", "valueB", true)); + initialProperties.add(new PluginProperty("keyC", "valueC", true)); + + final List priorNewProperties = new ArrayList(); + priorNewProperties.add(new PluginProperty("keyD", "valueD", true)); + final List priorRemovedProperties = new ArrayList(); + priorRemovedProperties.add(new PluginProperty("keyA", "valueA", true)); + plugin.setPriorCallProperties(priorNewProperties, priorRemovedProperties); + + final List onResultNewProperties = new ArrayList(); + onResultNewProperties.add(new PluginProperty("keyE", "valueE", true)); + final List onResultRemovedProperties = new ArrayList(); + onResultRemovedProperties.add(new PluginProperty("keyB", "valueB", true)); + plugin.setOnResultProperties(onResultNewProperties, onResultRemovedProperties); + + final List expectedPriorCallProperties = new ArrayList(); + expectedPriorCallProperties.add(new PluginProperty("keyB", "valueB", true)); + expectedPriorCallProperties.add(new PluginProperty("keyC", "valueC", true)); + expectedPriorCallProperties.add(new PluginProperty("keyD", "valueD", true)); + + validationPlugin.setExpectedPriorCallProperties(expectedPriorCallProperties); + + final List expectedProperties = new ArrayList(); + expectedProperties.add(new PluginProperty("keyC", "valueC", true)); + expectedProperties.add(new PluginProperty("keyD", "valueD", true)); + expectedProperties.add(new PluginProperty("keyE", "valueE", true)); + + validationPlugin.setExpectedProperties(expectedProperties); + + paymentGatewayApi.buildFormDescriptorWithPaymentControl(account, account.getPaymentMethodId(), ImmutableList.of(), initialProperties, paymentOptions, callContext); + + } + + public static class TestPaymentGatewayApiValidationPlugin implements PaymentControlPluginApi { + + public static final String VALIDATION_PLUGIN_NAME = "TestPaymentGatewayApiValidationPlugin"; + + private Iterable expectedPriorCallProperties; + private Iterable expectedProperties; + + public TestPaymentGatewayApiValidationPlugin() { + this.expectedPriorCallProperties = ImmutableList.of(); + this.expectedProperties = ImmutableList.of(); + } + + public void setExpectedProperties(final Iterable expectedProperties) { + this.expectedProperties = expectedProperties; + } + + public void setExpectedPriorCallProperties(final List expectedPriorCallProperties) { + this.expectedPriorCallProperties = expectedPriorCallProperties; + } + + @Override + public PriorPaymentControlResult priorCall(final PaymentControlContext paymentControlContext, final Iterable properties) throws PaymentControlApiException { + validate(properties, expectedPriorCallProperties); + return new DefaultPriorPaymentControlResult(false); + } + + @Override + public OnSuccessPaymentControlResult onSuccessCall(final PaymentControlContext paymentControlContext, final Iterable properties) throws PaymentControlApiException { + validate(properties, expectedProperties); + return new DefaultOnSuccessPaymentControlResult(); + } + + @Override + public OnFailurePaymentControlResult onFailureCall(final PaymentControlContext paymentControlContext, final Iterable properties) throws PaymentControlApiException { + validate(properties, expectedProperties); + return new DefaultFailureCallResult(); + } + + private static void validate(final Iterable properties, final Iterable expected) { + Assert.assertEquals(Iterables.size(properties), Iterables.size(expected), "Got " + Iterables.size(properties) + "properties" + ", expected " + Iterables.size(expected)); + + for (final PluginProperty curExpected : expected) { + Assert.assertTrue(Iterables.any(properties, new Predicate() { + @Override + public boolean apply(final PluginProperty input) { + return input.getKey().equals(curExpected.getKey()) && input.getValue().equals(curExpected.getValue()); + + } + }), "Cannot find expected property" + curExpected.getKey()); + } + } + + } + + public static class TestPaymentGatewayApiControlPlugin implements PaymentControlPluginApi { + + public static final String PLUGIN_NAME = "TestPaymentGatewayApiControlPlugin"; + + private Iterable newPriorCallProperties; + private Iterable removedPriorCallProperties; + + private Iterable newOnResultProperties; + private Iterable removedOnResultProperties; + + public TestPaymentGatewayApiControlPlugin() { + this.newPriorCallProperties = ImmutableList.of(); + this.removedPriorCallProperties = ImmutableList.of(); + this.newOnResultProperties = ImmutableList.of(); + this.removedOnResultProperties = ImmutableList.of(); + } + + public void setPriorCallProperties(final Iterable newPriorCallProperties, final Iterable removedPriorCallProperties) { + this.newPriorCallProperties = newPriorCallProperties; + this.removedPriorCallProperties = removedPriorCallProperties; + } + + public void setOnResultProperties(final Iterable newOnResultProperties, final Iterable removedOnResultProperties) { + this.newOnResultProperties = newOnResultProperties; + this.removedOnResultProperties = removedOnResultProperties; + } + + @Override + public PriorPaymentControlResult priorCall(final PaymentControlContext paymentControlContext, final Iterable properties) throws PaymentControlApiException { + return new DefaultPriorPaymentControlResult(false, null, null, null, getAdjustedProperties(properties, newPriorCallProperties, removedPriorCallProperties)); + } + + @Override + public OnSuccessPaymentControlResult onSuccessCall(final PaymentControlContext paymentControlContext, final Iterable properties) throws PaymentControlApiException { + return new DefaultOnSuccessPaymentControlResult(getAdjustedProperties(properties, newOnResultProperties, removedOnResultProperties)); + } + + @Override + public OnFailurePaymentControlResult onFailureCall(final PaymentControlContext paymentControlContext, final Iterable properties) throws PaymentControlApiException { + return new DefaultFailureCallResult(null, getAdjustedProperties(properties, newOnResultProperties, removedOnResultProperties)); + } + + private static Iterable getAdjustedProperties(final Iterable input, final Iterable newProperties, final Iterable removedProperties) { + final Iterable filtered = Iterables.filter(input, new Predicate() { + @Override + public boolean apply(final PluginProperty p) { + final boolean toBeRemoved = Iterables.any(removedProperties, new Predicate() { + @Override + public boolean apply(final PluginProperty a) { + return a.getKey().equals(p.getKey()) && a.getValue().equals(p.getValue()); + } + }); + return !toBeRemoved; + } + }); + return Iterables.concat(filtered, newProperties); + } + } + +} diff --git a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorNoDB.java b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorNoDB.java index 522f3c465f..fd355dc697 100644 --- a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorNoDB.java +++ b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorNoDB.java @@ -24,6 +24,7 @@ import org.killbill.billing.account.api.Account; import org.killbill.billing.payment.PaymentTestSuiteNoDB; import org.killbill.billing.payment.api.PaymentMethod; +import org.killbill.billing.payment.api.PaymentMethodPlugin; import org.killbill.billing.payment.api.PluginProperty; import org.killbill.billing.payment.provider.ExternalPaymentProviderPlugin; import org.mockito.Mockito; @@ -34,6 +35,28 @@ public class TestPaymentMethodProcessorNoDB extends PaymentTestSuiteNoDB { + @Test(groups = "fast") + public void testPaymentMethodExternalKeySetByPluginIfNonSpecified() throws Exception { + final Account account = Mockito.mock(Account.class); + final PaymentMethodPlugin paymentMethodPlugin = Mockito.mock(PaymentMethodPlugin.class); + final Iterable properties = ImmutableList.of(); + + final String paymentMethodExternalKey = UUID.randomUUID().toString(); + final UUID paymentMethodId1 = paymentMethodProcessor.addPaymentMethod(paymentMethodExternalKey, "__EXTERNAL_PAYMENT__", account, false, paymentMethodPlugin, properties, callContext, internalCallContext); + final PaymentMethod paymentMethod1 = paymentMethodProcessor.getPaymentMethodById(paymentMethodId1, false, false, properties, callContext, internalCallContext); + Assert.assertEquals(paymentMethod1.getExternalKey(), paymentMethodExternalKey); + + // By default, the external payment plugin sets the external payment method id to "unknown" + final UUID paymentMethodId2 = paymentMethodProcessor.addPaymentMethod(null, "__EXTERNAL_PAYMENT__", account, false, paymentMethodPlugin, properties, callContext, internalCallContext); + final PaymentMethod paymentMethod2 = paymentMethodProcessor.getPaymentMethodById(paymentMethodId2, false, false, properties, callContext, internalCallContext); + Assert.assertEquals(paymentMethod2.getExternalKey(), "unknown"); + + // Make sure we don't try to set the same external key if the plugin returns duplicate external ids + final UUID paymentMethodId3 = paymentMethodProcessor.addPaymentMethod(null, "__EXTERNAL_PAYMENT__", account, false, paymentMethodPlugin, properties, callContext, internalCallContext); + final PaymentMethod paymentMethod3 = paymentMethodProcessor.getPaymentMethodById(paymentMethodId3, false, false, properties, callContext, internalCallContext); + Assert.assertNotEquals(paymentMethod3.getExternalKey(), "unknown"); + } + @Test(groups = "fast") public void testGetExternalPaymentProviderPlugin() throws Exception { final Iterable properties = ImmutableList.of(); diff --git a/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTask.java b/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTask.java index 569070f1a9..9e81218a2a 100644 --- a/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTask.java +++ b/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTask.java @@ -35,7 +35,6 @@ public class TestIncompletePaymentTransactionTask extends PaymentTestSuiteNoDB { @Test(groups = "fast") public void testGetNextNotificationTime() { - assertNull(incompletePaymentTransactionTask.getNextNotificationTime(null)); final DateTime initTime = clock.getUTCNow(); // Based on config "15s,1m,3m,1h,1d,1d,1d,1d,1d" diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryAuthorizeOperationCallback.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryAuthorizeOperationCallback.java index f7007c01d2..2a3a677999 100644 --- a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryAuthorizeOperationCallback.java +++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryAuthorizeOperationCallback.java @@ -28,12 +28,14 @@ import org.killbill.billing.payment.api.TransactionStatus; import org.killbill.billing.payment.core.PaymentProcessor; import org.killbill.billing.payment.core.sm.control.AuthorizeControlOperation; +import org.killbill.billing.payment.core.sm.control.ControlPluginRunner; import org.killbill.billing.payment.core.sm.control.PaymentStateControlContext; import org.killbill.billing.payment.dao.PaymentDao; import org.killbill.billing.payment.dao.PaymentModelDao; import org.killbill.billing.payment.dao.PaymentTransactionModelDao; import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.clock.Clock; import org.killbill.commons.locker.GlobalLocker; @@ -45,8 +47,15 @@ public class MockRetryAuthorizeOperationCallback extends AuthorizeControlOperati private Exception exception; private OperationResult result; - public MockRetryAuthorizeOperationCallback(final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration retryPluginRegistry, final PaymentDao paymentDao, final Clock clock) { - super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, retryPluginRegistry); + public MockRetryAuthorizeOperationCallback(final GlobalLocker locker, + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, + final PaymentStateControlContext paymentStateContext, + final PaymentProcessor paymentProcessor, + final ControlPluginRunner controlPluginRunner, + final PaymentDao paymentDao, + final Clock clock) { + super(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner); this.paymentDao = paymentDao; this.clock = clock; } @@ -62,23 +71,23 @@ protected Payment doCallSpecificOperationCallback() throws PaymentApiException { } final PaymentModelDao payment = new PaymentModelDao(clock.getUTCNow(), clock.getUTCNow(), - paymentStateContext.account.getId(), - paymentStateContext.paymentMethodId, - paymentStateContext.paymentExternalKey); + paymentStateContext.getAccount().getId(), + paymentStateContext.getPaymentMethodId(), + paymentStateContext.getPaymentExternalKey()); final PaymentTransactionModelDao transaction = new PaymentTransactionModelDao(clock.getUTCNow(), clock.getUTCNow(), paymentStateContext.getAttemptId(), - paymentStateContext.paymentTransactionExternalKey, - paymentStateContext.paymentId, - paymentStateContext.transactionType, + paymentStateContext.getPaymentTransactionExternalKey(), + paymentStateContext.getPaymentId(), + paymentStateContext.getTransactionType(), clock.getUTCNow(), TransactionStatus.SUCCESS, - paymentStateContext.amount, - paymentStateContext.currency, + paymentStateContext.getAmount(), + paymentStateContext.getCurrency(), "", ""); - final PaymentModelDao paymentModelDao = paymentDao.insertPaymentWithFirstTransaction(payment, transaction, paymentStateContext.internalCallContext); + final PaymentModelDao paymentModelDao = paymentDao.insertPaymentWithFirstTransaction(payment, transaction, paymentStateContext.getInternalCallContext()); final PaymentTransaction convertedTransaction = new DefaultPaymentTransaction(transaction.getId(), paymentStateContext.getAttemptId(), transaction.getTransactionExternalKey(), diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java index 7a69891ff0..07c8031c0e 100644 --- a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java +++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java @@ -20,7 +20,6 @@ import java.math.BigDecimal; import java.util.List; import java.util.UUID; -import java.util.concurrent.ExecutorService; import javax.annotation.Nullable; import javax.inject.Inject; @@ -28,22 +27,22 @@ import org.killbill.automaton.Operation.OperationCallback; import org.killbill.automaton.OperationResult; -import org.killbill.automaton.StateMachineConfig; import org.killbill.billing.account.api.Account; import org.killbill.billing.callcontext.InternalCallContext; import org.killbill.billing.catalog.api.Currency; +import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; import org.killbill.billing.osgi.api.OSGIServiceRegistration; import org.killbill.billing.payment.api.PaymentApiException; import org.killbill.billing.payment.api.PluginProperty; import org.killbill.billing.payment.api.TransactionType; +import org.killbill.billing.payment.core.PaymentExecutors; import org.killbill.billing.payment.core.PaymentProcessor; +import org.killbill.billing.payment.core.sm.control.ControlPluginRunner; import org.killbill.billing.payment.core.sm.control.PaymentStateControlContext; import org.killbill.billing.payment.dao.PaymentDao; import org.killbill.billing.payment.dispatcher.PluginDispatcher; -import org.killbill.billing.payment.glue.PaymentModule; import org.killbill.billing.payment.plugin.api.PaymentPluginApi; import org.killbill.billing.payment.retry.BaseRetryService.RetryServiceScheduler; -import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; import org.killbill.billing.tag.TagInternalApi; import org.killbill.billing.util.callcontext.CallContext; import org.killbill.billing.util.config.PaymentConfig; @@ -51,7 +50,6 @@ import org.killbill.clock.Clock; import org.killbill.commons.locker.GlobalLocker; -import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED; import static org.killbill.billing.payment.glue.PaymentModule.RETRYABLE_NAMED; public class MockRetryablePaymentAutomatonRunner extends PluginControlPaymentAutomatonRunner { @@ -60,10 +58,10 @@ public class MockRetryablePaymentAutomatonRunner extends PluginControlPaymentAut private PaymentStateControlContext context; @Inject - public MockRetryablePaymentAutomatonRunner(@Named(PaymentModule.STATE_MACHINE_PAYMENT) final StateMachineConfig stateMachineConfig, @Named(PaymentModule.STATE_MACHINE_RETRY) final StateMachineConfig retryStateMachine, final PaymentDao paymentDao, final GlobalLocker locker, final OSGIServiceRegistration pluginRegistry, final OSGIServiceRegistration retryPluginRegistry, final Clock clock, final TagInternalApi tagApi, final PaymentProcessor paymentProcessor, - @Named(RETRYABLE_NAMED) final RetryServiceScheduler retryServiceScheduler, final PaymentConfig paymentConfig, @com.google.inject.name.Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor, - final PaymentStateMachineHelper paymentSMHelper, final PaymentControlStateMachineHelper retrySMHelper, final PersistentBus eventBus) { - super(stateMachineConfig, paymentDao, locker, pluginRegistry, retryPluginRegistry, clock, paymentProcessor, retryServiceScheduler, paymentConfig, executor, paymentSMHelper, retrySMHelper, eventBus); + public MockRetryablePaymentAutomatonRunner(final PaymentDao paymentDao, final GlobalLocker locker, final OSGIServiceRegistration pluginRegistry, final OSGIServiceRegistration retryPluginRegistry, final Clock clock, final TagInternalApi tagApi, final PaymentProcessor paymentProcessor, + @Named(RETRYABLE_NAMED) final RetryServiceScheduler retryServiceScheduler, final PaymentConfig paymentConfig, final PaymentExecutors executors, + final PaymentStateMachineHelper paymentSMHelper, final PaymentControlStateMachineHelper retrySMHelper, final ControlPluginRunner controlPluginRunner, final PersistentBus eventBus) { + super(paymentDao, locker, pluginRegistry, retryPluginRegistry, clock, paymentProcessor, retryServiceScheduler, paymentConfig, executors, paymentSMHelper, retrySMHelper, controlPluginRunner, eventBus); } @Override diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java index 36800f1bb4..1717dc49c3 100644 --- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java +++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java @@ -134,8 +134,8 @@ public void testLeaveStateForConflictingPaymentTransactionExternalKeyAcrossAccou paymentStateContext.getPaymentMethodId(), paymentStateContext.getAmount(), paymentStateContext.getCurrency(), - paymentStateContext.shouldLockAccountAndDispatch, - paymentStateContext.overridePluginOperationResult, + paymentStateContext.shouldLockAccountAndDispatch(), + paymentStateContext.getOverridePluginOperationResult(), paymentStateContext.getProperties(), internalCallContextForOtherAccount, callContext); diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentOperation.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentOperation.java index 586e7597f2..1765befe65 100644 --- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentOperation.java +++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentOperation.java @@ -19,7 +19,6 @@ import java.math.BigDecimal; import java.util.UUID; -import java.util.concurrent.Executors; import javax.annotation.Nullable; @@ -39,6 +38,7 @@ import org.killbill.billing.payment.plugin.api.PaymentPluginStatus; import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; import org.killbill.billing.payment.provider.MockPaymentProviderPlugin; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; import org.killbill.commons.locker.memory.MemoryGlobalLocker; import org.mockito.Mockito; @@ -104,7 +104,7 @@ public void testPaymentSuccess() throws Exception { private void setUp(final PaymentPluginStatus paymentPluginStatus) throws Exception { final GlobalLocker locker = new MemoryGlobalLocker(); - final PluginDispatcher paymentPluginDispatcher = new PluginDispatcher(1, Executors.newCachedThreadPool()); + final PluginDispatcher paymentPluginDispatcher = new PluginDispatcher(1, paymentExecutors); paymentStateContext = new PaymentStateContext(true, UUID.randomUUID(), null, null, @@ -126,7 +126,7 @@ private void setUp(final PaymentPluginStatus paymentPluginStatus) throws Excepti Mockito.when(paymentDao.getPaymentMethodIncludedDeleted(paymentStateContext.getPaymentMethodId(), internalCallContext)).thenReturn(paymentMethodModelDao); final PaymentAutomatonDAOHelper daoHelper = new PaymentAutomatonDAOHelper(paymentStateContext, clock.getUTCNow(), paymentDao, registry, internalCallContext, eventBus, paymentSMHelper); - paymentOperation = new PaymentOperationTest(paymentPluginStatus, daoHelper, locker, paymentPluginDispatcher, paymentStateContext); + paymentOperation = new PaymentOperationTest(paymentPluginStatus, daoHelper, locker, paymentPluginDispatcher, paymentConfig, paymentStateContext); } private static final class PaymentOperationTest extends PaymentOperation { @@ -135,8 +135,10 @@ private static final class PaymentOperationTest extends PaymentOperation { public PaymentOperationTest(@Nullable final PaymentPluginStatus paymentPluginStatus, final PaymentAutomatonDAOHelper daoHelper, final GlobalLocker locker, - final PluginDispatcher paymentPluginDispatcher, final PaymentStateContext paymentStateContext) throws PaymentApiException { - super(locker, daoHelper, paymentPluginDispatcher, paymentStateContext); + final PluginDispatcher paymentPluginDispatcher, + final PaymentConfig paymentConfig, + final PaymentStateContext paymentStateContext) throws PaymentApiException { + super(locker, daoHelper, paymentPluginDispatcher, paymentConfig, paymentStateContext); this.paymentInfoPlugin = (paymentPluginStatus == null ? null : getPaymentInfoPlugin(paymentPluginStatus)); } diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java index f004f02003..75e39c3729 100644 --- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java +++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java @@ -20,7 +20,6 @@ import java.math.BigDecimal; import java.util.UUID; import java.util.concurrent.Callable; -import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -36,12 +35,13 @@ import org.killbill.billing.payment.api.PaymentApiException; import org.killbill.billing.payment.api.PluginProperty; import org.killbill.billing.payment.api.TransactionType; -import org.killbill.billing.payment.core.ProcessorBase.WithAccountLockCallback; +import org.killbill.billing.payment.core.ProcessorBase.DispatcherCallback; import org.killbill.billing.payment.core.sm.payments.PaymentOperation; import org.killbill.billing.payment.dispatcher.PluginDispatcher; import org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType; import org.killbill.billing.payment.plugin.api.PaymentPluginApiException; import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; +import org.killbill.billing.util.config.PaymentConfig; import org.killbill.commons.locker.GlobalLocker; import org.killbill.commons.locker.memory.MemoryGlobalLocker; import org.mockito.Mockito; @@ -184,7 +184,7 @@ private PaymentOperation getPluginOperation(final boolean shouldLockAccount) thr } private PaymentOperation getPluginOperation(final boolean shouldLockAccount, final int timeoutSeconds) throws PaymentApiException { - final PluginDispatcher paymentPluginDispatcher = new PluginDispatcher(timeoutSeconds, Executors.newCachedThreadPool()); + final PluginDispatcher paymentPluginDispatcher = new PluginDispatcher(timeoutSeconds, paymentExecutors); final PaymentStateContext paymentStateContext = new PaymentStateContext(true, UUID.randomUUID(), null, null, @@ -202,10 +202,10 @@ private PaymentOperation getPluginOperation(final boolean shouldLockAccount, fin final PaymentAutomatonDAOHelper daoHelper = Mockito.mock(PaymentAutomatonDAOHelper.class); Mockito.when(daoHelper.getPaymentProviderPlugin()).thenReturn(null); - return new PluginOperationTest(daoHelper, locker, paymentPluginDispatcher, paymentStateContext); + return new PluginOperationTest(daoHelper, locker, paymentPluginDispatcher, paymentConfig, paymentStateContext); } - private static final class CallbackTest implements WithAccountLockCallback, PaymentApiException> { + private static final class CallbackTest implements DispatcherCallback, PaymentApiException> { private final AtomicInteger runCount = new AtomicInteger(0); @@ -274,8 +274,8 @@ public int getRunCount() { private static final class PluginOperationTest extends PaymentOperation { - protected PluginOperationTest(final PaymentAutomatonDAOHelper daoHelper, final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, final PaymentStateContext paymentStateContext) throws PaymentApiException { - super(locker, daoHelper, paymentPluginDispatcher, paymentStateContext); + protected PluginOperationTest(final PaymentAutomatonDAOHelper daoHelper, final GlobalLocker locker, final PluginDispatcher paymentPluginDispatcher, final PaymentConfig paymentConfig, final PaymentStateContext paymentStateContext) throws PaymentApiException { + super(locker, daoHelper, paymentPluginDispatcher, paymentConfig, paymentStateContext); } @Override diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java index b016e7a4a1..a64c15a567 100644 --- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java +++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java @@ -20,7 +20,6 @@ import java.math.BigDecimal; import java.util.List; import java.util.UUID; -import java.util.concurrent.ExecutorService; import javax.inject.Named; @@ -32,6 +31,7 @@ import org.killbill.billing.account.api.Account; import org.killbill.billing.callcontext.InternalTenantContext; import org.killbill.billing.catalog.api.Currency; +import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; import org.killbill.billing.osgi.api.OSGIServiceDescriptor; import org.killbill.billing.osgi.api.OSGIServiceRegistration; import org.killbill.billing.payment.PaymentTestSuiteNoDB; @@ -39,8 +39,10 @@ import org.killbill.billing.payment.api.PluginProperty; import org.killbill.billing.payment.api.TransactionStatus; import org.killbill.billing.payment.api.TransactionType; +import org.killbill.billing.payment.core.PaymentExecutors; import org.killbill.billing.payment.core.PaymentProcessor; import org.killbill.billing.payment.core.PluginControlPaymentProcessor; +import org.killbill.billing.payment.core.sm.control.ControlPluginRunner; import org.killbill.billing.payment.core.sm.control.PaymentStateControlContext; import org.killbill.billing.payment.dao.MockPaymentDao; import org.killbill.billing.payment.dao.PaymentAttemptModelDao; @@ -52,7 +54,6 @@ import org.killbill.billing.payment.plugin.api.PaymentPluginApi; import org.killbill.billing.payment.provider.MockPaymentControlProviderPlugin; import org.killbill.billing.payment.retry.BaseRetryService.RetryServiceScheduler; -import org.killbill.billing.control.plugin.api.PaymentControlPluginApi; import org.killbill.billing.tag.TagInternalApi; import org.killbill.billing.util.callcontext.InternalCallContextFactory; import org.killbill.billing.util.dao.NonEntityDao; @@ -71,7 +72,6 @@ import com.google.common.collect.Iterables; import com.google.inject.Inject; -import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED; import static org.killbill.billing.payment.glue.PaymentModule.RETRYABLE_NAMED; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -102,13 +102,14 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB { @Named(RETRYABLE_NAMED) private RetryServiceScheduler retryServiceScheduler; @Inject - @Named(PLUGIN_EXECUTOR_NAMED) - private ExecutorService executor; + private PaymentExecutors executors; @Inject private PaymentStateMachineHelper paymentSMHelper; @Inject private PaymentControlStateMachineHelper retrySMHelper; @Inject + private ControlPluginRunner controlPluginRunner; + @Inject private InternalCallContextFactory internalCallContextFactory; private Account account; @@ -155,8 +156,6 @@ public void beforeMethod() throws Exception { this.utcNow = clock.getUTCNow(); runner = new MockRetryablePaymentAutomatonRunner( - stateMachineConfig, - retryStateMachineConfig, paymentDao, locker, pluginRegistry, @@ -166,32 +165,34 @@ public void beforeMethod() throws Exception { paymentProcessor, retryServiceScheduler, paymentConfig, - executor, + paymentExecutors, paymentSMHelper, retrySMHelper, + controlPluginRunner, eventBus); paymentStateContext = new PaymentStateControlContext(ImmutableList.of(MockPaymentControlProviderPlugin.PLUGIN_NAME), - true, - null, - paymentExternalKey, - paymentTransactionExternalKey, - TransactionType.AUTHORIZE, - account, - paymentMethodId, - amount, - currency, - emptyProperties, - internalCallContext, - callContext); + true, + null, + paymentExternalKey, + paymentTransactionExternalKey, + TransactionType.AUTHORIZE, + account, + paymentMethodId, + amount, + currency, + emptyProperties, + internalCallContext, + callContext); mockRetryAuthorizeOperationCallback = new MockRetryAuthorizeOperationCallback(locker, runner.getPaymentPluginDispatcher(), + paymentConfig, paymentStateContext, null, - runner.getRetryPluginRegistry(), + controlPluginRunner, paymentDao, clock); @@ -201,7 +202,6 @@ public void beforeMethod() throws Exception { tagApi, paymentDao, locker, - executor, internalCallContextFactory, runner, retrySMHelper, diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java new file mode 100644 index 0000000000..e48f3b4fdb --- /dev/null +++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java @@ -0,0 +1,74 @@ +/* + * 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.payment.core.sm.control; + +import java.math.BigDecimal; +import java.util.UUID; + +import org.killbill.billing.account.api.Account; +import org.killbill.billing.catalog.api.Currency; +import org.killbill.billing.control.plugin.api.PaymentApiType; +import org.killbill.billing.control.plugin.api.PriorPaymentControlResult; +import org.killbill.billing.payment.PaymentTestSuiteNoDB; +import org.killbill.billing.payment.api.PluginProperty; +import org.killbill.billing.payment.api.TransactionType; +import org.killbill.billing.payment.provider.DefaultPaymentControlProviderPluginRegistry; +import org.killbill.billing.util.UUIDs; +import org.mockito.Mockito; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableList; + +public class TestControlPluginRunner extends PaymentTestSuiteNoDB { + + @Test(groups = "fast") + public void testPriorCallWithUnknownPlugin() throws Exception { + final Account account = Mockito.mock(Account.class); + final UUID paymentMethodId = UUIDs.randomUUID(); + final UUID paymentId = UUIDs.randomUUID(); + final String paymentExternalKey = UUIDs.randomUUID().toString(); + final String paymentTransactionExternalKey = UUIDs.randomUUID().toString(); + final BigDecimal amount = BigDecimal.ONE; + final Currency currency = Currency.USD; + final ImmutableList paymentControlPluginNames = ImmutableList.of("not-registered"); + final ImmutableList pluginProperties = ImmutableList.of(); + + final ControlPluginRunner controlPluginRunner = new ControlPluginRunner(new DefaultPaymentControlProviderPluginRegistry()); + final PriorPaymentControlResult paymentControlResult = controlPluginRunner.executePluginPriorCalls(account, + paymentMethodId, + null, + paymentId, + paymentExternalKey, + paymentTransactionExternalKey, + PaymentApiType.PAYMENT_TRANSACTION, + TransactionType.AUTHORIZE, + null, + amount, + currency, + true, + paymentControlPluginNames, + pluginProperties, + callContext); + Assert.assertEquals(paymentControlResult.getAdjustedAmount(), amount); + Assert.assertEquals(paymentControlResult.getAdjustedCurrency(), currency); + Assert.assertEquals(paymentControlResult.getAdjustedPaymentMethodId(), paymentMethodId); + Assert.assertEquals(paymentControlResult.getAdjustedPluginProperties(), pluginProperties); + Assert.assertFalse(paymentControlResult.isAborted()); + } +} diff --git a/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java b/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java index d226d736b9..835df39e38 100644 --- a/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java +++ b/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java @@ -26,12 +26,27 @@ import org.killbill.billing.payment.PaymentTestSuiteNoDB; import org.killbill.billing.payment.api.PaymentApiException; import org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType; +import org.killbill.commons.profiling.Profiling; +import org.killbill.commons.request.Request; +import org.killbill.commons.request.RequestData; import org.testng.Assert; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; public class TestPluginDispatcher extends PaymentTestSuiteNoDB { - private final PluginDispatcher voidPluginDispatcher = new PluginDispatcher(10, Executors.newSingleThreadExecutor()); + private PluginDispatcher voidPluginDispatcher; + + private PluginDispatcher stringPluginDispatcher; + + @BeforeMethod(groups = "fast") + public void beforeMethod() throws Exception { + super.beforeMethod(); + eventBus.start(); + voidPluginDispatcher = new PluginDispatcher(10, paymentExecutors); + stringPluginDispatcher = new PluginDispatcher(1, paymentExecutors); + } + @Test(groups = "fast") public void testDispatchWithTimeout() throws TimeoutException, PaymentApiException { @@ -106,4 +121,25 @@ public PluginDispatcherReturnType call() throws Exception { } Assert.assertTrue(gotIt); } + + + @Test(groups = "fast") + public void testDispatchWithRequestData() throws TimeoutException, PaymentApiException, ExecutionException, InterruptedException { + + final String requestId = "vive la vie et les coquillettes"; + + final Callable> delegate = new Callable>() { + @Override + public PluginDispatcherReturnType call() throws Exception { + return PluginDispatcher.createPluginDispatcherReturnType(Request.getPerThreadRequestData().getRequestId()); + } + }; + + final CallableWithRequestData> callable = new CallableWithRequestData>(new RequestData(requestId), + delegate); + + final String actualRequestId = stringPluginDispatcher.dispatchWithTimeout(callable, 100, TimeUnit.MILLISECONDS); + Assert.assertEquals(actualRequestId, requestId); + } + } diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java index f161297417..f3f94e7bbe 100644 --- a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java +++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java @@ -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 @@ -20,6 +20,7 @@ import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.UUID; @@ -33,8 +34,8 @@ import org.killbill.billing.payment.api.TransactionType; import org.killbill.billing.payment.plugin.api.GatewayNotification; import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor; -import org.killbill.billing.payment.plugin.api.NoOpPaymentPluginApi; import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin; +import org.killbill.billing.payment.plugin.api.PaymentPluginApi; import org.killbill.billing.payment.plugin.api.PaymentPluginApiException; import org.killbill.billing.payment.plugin.api.PaymentPluginStatus; import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin; @@ -53,7 +54,12 @@ * This MockPaymentProviderPlugin only works for a single accounts as we don't specify the accountId * for operations such as addPaymentMethod. */ -public class MockPaymentProviderPlugin implements NoOpPaymentPluginApi { +public class MockPaymentProviderPlugin implements PaymentPluginApi { + + public static final String GATEWAY_ERROR_CODE = "gatewayErrorCode"; + public static final String GATEWAY_ERROR = "gatewayError"; + + public static final String PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE = "paymentPluginStatusOverride"; public static final String PLUGIN_NAME = "__NO_OP__"; @@ -179,7 +185,6 @@ public MockPaymentProviderPlugin(final Clock clock) { clear(); } - @Override public void clear() { makeNextInvoiceFailWithException.set(false); makeAllInvoicesFailWithError.set(false); @@ -190,48 +195,51 @@ public void clear() { paymentMethodsInfo.clear(); } - @Override public void makeNextPaymentFailWithError() { makeNextInvoiceFailWithError.set(true); } - @Override public void makeNextPaymentFailWithException() { makeNextInvoiceFailWithException.set(true); } - @Override public void makeAllInvoicesFailWithError(final boolean failure) { makeAllInvoicesFailWithError.set(failure); } + public void updatePaymentTransactions(final UUID paymentId, final List newTransactions) { + if (paymentTransactions.containsKey(paymentId.toString())) { + paymentTransactions.put (paymentId.toString(), newTransactions); + } + } + @Override public PaymentTransactionInfoPlugin authorizePayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable properties, final CallContext context) throws PaymentPluginApiException { - return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.AUTHORIZE, amount, currency); + return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.AUTHORIZE, amount, currency, properties); } @Override public PaymentTransactionInfoPlugin capturePayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable properties, final CallContext context) throws PaymentPluginApiException { - return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.CAPTURE, amount, currency); + return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.CAPTURE, amount, currency, properties); } @Override public PaymentTransactionInfoPlugin purchasePayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable properties, final CallContext context) throws PaymentPluginApiException { - return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.PURCHASE, amount, currency); + return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.PURCHASE, amount, currency, properties); } @Override public PaymentTransactionInfoPlugin voidPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final Iterable properties, final CallContext context) throws PaymentPluginApiException { - return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.VOID, BigDecimal.ZERO, null); + return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.VOID, BigDecimal.ZERO, null, properties); } @Override public PaymentTransactionInfoPlugin creditPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final Iterable properties, final CallContext context) throws PaymentPluginApiException { - return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.CREDIT, amount, currency); + return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.CREDIT, amount, currency, properties); } @Override @@ -305,16 +313,16 @@ public void resetPaymentMethods(final UUID kbAccountId, final List customFields, final Iterable properties, final CallContext callContext) { - return null; + return new DefaultNoOpHostedPaymentPageFormDescriptor(kbAccountId); } @Override public GatewayNotification processNotification(final String notification, final Iterable properties, final CallContext callContext) throws PaymentPluginApiException { - return null; + return new DefaultNoOpGatewayNotification(); } @Override - public PaymentTransactionInfoPlugin refundPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbPaymentMethodId, final UUID kbTransactionId, final BigDecimal refundAmount, final Currency currency, final Iterable properties, final CallContext context) throws PaymentPluginApiException { + public PaymentTransactionInfoPlugin refundPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal refundAmount, final Currency currency, final Iterable properties, final CallContext context) throws PaymentPluginApiException { final InternalPaymentInfo info = payments.get(kbPaymentId.toString()); if (info == null) { @@ -325,32 +333,54 @@ public PaymentTransactionInfoPlugin refundPayment(final UUID kbAccountId, final throw new PaymentPluginApiException("", String.format("Refund amount of %s for payment id %s is bigger than the payment amount %s (plugin %s)", refundAmount, kbPaymentId.toString(), maxAmountRefundable, PLUGIN_NAME)); } - return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.REFUND, refundAmount, currency); + return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.REFUND, refundAmount, currency, properties); } - private PaymentTransactionInfoPlugin getPaymentTransactionInfoPluginResult(final UUID kbPaymentId, final UUID kbTransactionId, final TransactionType type, final BigDecimal amount, final Currency currency) throws PaymentPluginApiException { - + private PaymentTransactionInfoPlugin getPaymentTransactionInfoPluginResult(final UUID kbPaymentId, final UUID kbTransactionId, final TransactionType type, final BigDecimal amount, final Currency currency, final Iterable pluginProperties) throws PaymentPluginApiException { if (makeNextInvoiceFailWithException.getAndSet(false)) { throw new PaymentPluginApiException("", "test error"); } - final PaymentPluginStatus status = (makeAllInvoicesFailWithError.get() || makeNextInvoiceFailWithError.getAndSet(false)) ? PaymentPluginStatus.ERROR : PaymentPluginStatus.PROCESSED; + final PluginProperty paymentPluginStatusOverride = Iterables.tryFind(pluginProperties, new Predicate() { + @Override + public boolean apply(final PluginProperty input) { + return PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE.equals(input.getKey()); + } + }).orNull(); + + final PaymentPluginStatus status; + if (paymentPluginStatusOverride != null && paymentPluginStatusOverride.getValue() != null) { + status = PaymentPluginStatus.valueOf(paymentPluginStatusOverride.getValue().toString()); + } else { + status = (makeAllInvoicesFailWithError.get() || makeNextInvoiceFailWithError.getAndSet(false)) ? PaymentPluginStatus.ERROR : PaymentPluginStatus.PROCESSED; + } + final String errorCode = status == PaymentPluginStatus.PROCESSED ? "" : GATEWAY_ERROR_CODE; + final String error = status == PaymentPluginStatus.PROCESSED ? "" : GATEWAY_ERROR; InternalPaymentInfo info = payments.get(kbPaymentId.toString()); if (info == null) { info = new InternalPaymentInfo(); payments.put(kbPaymentId.toString(), info); } - info.addAmount(type, amount); - final PaymentTransactionInfoPlugin result = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, type, amount, currency, clock.getUTCNow(), clock.getUTCNow(), status, null); + final PaymentTransactionInfoPlugin result = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, type, amount, currency, clock.getUTCNow(), clock.getUTCNow(), status, errorCode, error); List existingTransactions = paymentTransactions.get(kbPaymentId.toString()); if (existingTransactions == null) { existingTransactions = new ArrayList(); paymentTransactions.put(kbPaymentId.toString(), existingTransactions); } + final Iterator iterator = existingTransactions.iterator(); + while (iterator.hasNext()) { + final PaymentTransactionInfoPlugin existingTransaction = iterator.next(); + if (existingTransaction.getKbTransactionPaymentId().equals(kbTransactionId)) { + info.addAmount(type, existingTransaction.getAmount().negate()); + iterator.remove(); + } + } existingTransactions.add(result); + info.addAmount(type, result.getAmount()); + return result; } } diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/TestDefaultNoOpPaymentInfoPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/TestDefaultNoOpPaymentInfoPlugin.java index c4efb54921..e0a40d7995 100644 --- a/payment/src/test/java/org/killbill/billing/payment/provider/TestDefaultNoOpPaymentInfoPlugin.java +++ b/payment/src/test/java/org/killbill/billing/payment/provider/TestDefaultNoOpPaymentInfoPlugin.java @@ -41,15 +41,15 @@ public void testEquals() throws Exception { final String error = UUID.randomUUID().toString(); final DefaultNoOpPaymentInfoPlugin info = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.PURCHASE, amount, Currency.USD, effectiveDate, createdDate, - status, error); + status, error, null); Assert.assertEquals(info, info); final DefaultNoOpPaymentInfoPlugin sameInfo = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.PURCHASE, amount, Currency.USD, effectiveDate, createdDate, - status, error); + status, error, null); Assert.assertEquals(sameInfo, info); final DefaultNoOpPaymentInfoPlugin otherInfo = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, kbTransactionId, TransactionType.PURCHASE, amount, Currency.USD, effectiveDate, createdDate, - status, UUID.randomUUID().toString()); + status, UUID.randomUUID().toString(), null); Assert.assertNotEquals(otherInfo, info); } } diff --git a/pom.xml b/pom.xml index 5ee6a68d68..8bf3ba3738 100644 --- a/pom.xml +++ b/pom.xml @@ -21,10 +21,10 @@ killbill-oss-parent org.kill-bill.billing - 0.31 + 0.52 killbill - 0.15.3-SNAPSHOT + 0.15.7-SNAPSHOT pom killbill Library for managing recurring subscriptions and the associated billing diff --git a/profiles/killbill/pom.xml b/profiles/killbill/pom.xml index 47ce217769..47073ebb39 100644 --- a/profiles/killbill/pom.xml +++ b/profiles/killbill/pom.xml @@ -21,7 +21,7 @@ killbill-profiles org.kill-bill.billing - 0.15.3-SNAPSHOT + 0.15.7-SNAPSHOT ../pom.xml killbill-profiles-killbill diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/filters/RequestDataFilter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/filters/RequestDataFilter.java new file mode 100644 index 0000000000..a32a507291 --- /dev/null +++ b/profiles/killbill/src/main/java/org/killbill/billing/server/filters/RequestDataFilter.java @@ -0,0 +1,51 @@ +/* + * 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.server.filters; + +import java.util.List; + +import org.killbill.billing.util.UUIDs; +import org.killbill.commons.request.Request; +import org.killbill.commons.request.RequestData; + +import com.google.inject.Singleton; +import com.sun.jersey.spi.container.ContainerRequest; +import com.sun.jersey.spi.container.ContainerRequestFilter; +import com.sun.jersey.spi.container.ContainerResponse; +import com.sun.jersey.spi.container.ContainerResponseFilter; + +@Singleton +public class RequestDataFilter implements ContainerRequestFilter, ContainerResponseFilter { + + + private static final String REQUEST_ID_HEADER_REQ = "X-Killbill-Request-Id-Req"; + + @Override + public ContainerRequest filter(final ContainerRequest request) { + final List requestIdHeaderRequests = request.getRequestHeader(REQUEST_ID_HEADER_REQ); + final String requestId = (requestIdHeaderRequests == null || requestIdHeaderRequests.isEmpty()) ? UUIDs.randomUUID().toString() : requestIdHeaderRequests.get(0); + Request.setPerThreadRequestData(new RequestData(requestId)); + return request; + } + + @Override + public ContainerResponse filter(final ContainerRequest request, final ContainerResponse response) { + Request.resetPerThreadRequestData(); + return response; + } +} diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/listeners/KillbillGuiceListener.java b/profiles/killbill/src/main/java/org/killbill/billing/server/listeners/KillbillGuiceListener.java index 6e506da45e..e031a5d507 100644 --- a/profiles/killbill/src/main/java/org/killbill/billing/server/listeners/KillbillGuiceListener.java +++ b/profiles/killbill/src/main/java/org/killbill/billing/server/listeners/KillbillGuiceListener.java @@ -28,6 +28,7 @@ import org.killbill.billing.platform.api.KillbillConfigSource; import org.killbill.billing.platform.config.DefaultKillbillConfigSource; import org.killbill.billing.server.filters.ProfilingContainerResponseFilter; +import org.killbill.billing.server.filters.RequestDataFilter; import org.killbill.billing.server.filters.ResponseCorsFilter; import org.killbill.billing.server.modules.KillbillServerModule; import org.killbill.billing.server.security.TenantFilter; @@ -41,6 +42,7 @@ import com.google.common.collect.ImmutableMap; import com.google.inject.Module; import com.google.inject.servlet.ServletModule; +import com.sun.jersey.api.container.filter.GZIPContentEncodingFilter; import com.wordnik.swagger.jaxrs.config.BeanConfig; public class KillbillGuiceListener extends KillbillPlatformGuiceListener { @@ -70,9 +72,14 @@ protected ServletModule getServletModule() { // c.s.j.s.w.g.AbstractWadlGeneratorGrammarGenerator - Couldn't find grammar element for class javax.ws.rs.core.Response builder.addJerseyParam("com.sun.jersey.config.feature.DisableWADL", "true"); - // The logging filter is still incompatible with the GZIP filter - //builder.addJerseyFilter(GZIPContentEncodingFilter.class.getName()); + // In order to use the GZIPContentEncodingFilter, the jersey param "com.sun.jersey.config.feature.logging.DisableEntitylogging" + // must not be set to false. + if (config.isConfiguredToReturnGZIPResponses()) { + logger.info("Enable http gzip responses"); + builder.addJerseyFilter(GZIPContentEncodingFilter.class.getName()); + } builder.addJerseyFilter(ProfilingContainerResponseFilter.class.getName()); + builder.addJerseyFilter(RequestDataFilter.class.getName()); // Broader, to support the "Try it out!" feature //builder.addFilter("/" + SWAGGER_PATH + "*", ResponseCorsFilter.class); diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java index a67a7d5f85..7322ef04f1 100644 --- a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java +++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java @@ -26,6 +26,7 @@ import org.killbill.billing.currency.glue.CurrencyModule; import org.killbill.billing.entitlement.glue.DefaultEntitlementModule; import org.killbill.billing.invoice.glue.DefaultInvoiceModule; +import org.killbill.billing.jaxrs.glue.DefaultJaxrsModule; import org.killbill.billing.jaxrs.resources.AccountResource; import org.killbill.billing.jaxrs.resources.AdminResource; import org.killbill.billing.jaxrs.resources.BundleResource; @@ -161,6 +162,7 @@ protected void installKillbillModules() { install(new TemplateModule(configSource)); install(new DefaultTenantModule(configSource)); install(new UsageModule(configSource)); + install(new DefaultJaxrsModule(configSource)); } protected void configureResources() { diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java index 8955201408..85b17d9bbd 100644 --- a/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java +++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java @@ -27,6 +27,7 @@ import org.apache.shiro.codec.Base64; import org.apache.shiro.realm.jdbc.JdbcRealm; import org.apache.shiro.util.ByteSource; +import org.killbill.billing.util.config.SecurityConfig; import org.killbill.billing.util.security.shiro.KillbillCredentialsMatcher; /** @@ -37,11 +38,13 @@ public class KillbillJdbcTenantRealm extends JdbcRealm { private static final String KILLBILL_AUTHENTICATION_QUERY = "select api_secret, api_salt from tenants where api_key = ?"; private final DataSource dataSource; + private final SecurityConfig securityConfig; - public KillbillJdbcTenantRealm(final DataSource dataSource) { + public KillbillJdbcTenantRealm(final DataSource dataSource, final SecurityConfig securityConfig) { super(); this.dataSource = dataSource; + this.securityConfig = securityConfig; configureSecurity(); configureQueries(); @@ -61,7 +64,7 @@ protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken t private void configureSecurity() { setSaltStyle(SaltStyle.COLUMN); - setCredentialsMatcher(KillbillCredentialsMatcher.getCredentialsMatcher()); + setCredentialsMatcher(KillbillCredentialsMatcher.getCredentialsMatcher(securityConfig)); } private void configureQueries() { diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java index 4fb769e14d..97e2bedfd6 100644 --- a/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java +++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java @@ -44,6 +44,7 @@ import org.killbill.billing.tenant.api.Tenant; import org.killbill.billing.tenant.api.TenantApiException; import org.killbill.billing.tenant.api.TenantUserApi; +import org.killbill.billing.util.config.SecurityConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,6 +60,8 @@ public class TenantFilter implements Filter { @Inject protected TenantUserApi tenantUserApi; + @Inject + protected SecurityConfig securityConfig; @Inject @Named(KillbillPlatformModule.SHIRO_DATA_SOURCE_ID_NAMED) @@ -68,7 +71,7 @@ public class TenantFilter implements Filter { @Override public void init(final FilterConfig filterConfig) throws ServletException { - final Realm killbillJdbcTenantRealm = new KillbillJdbcTenantRealm(dataSource); + final Realm killbillJdbcTenantRealm = new KillbillJdbcTenantRealm(dataSource, securityConfig); // We use Shiro to verify the api credentials - but the Shiro Subject is only used for RBAC modularRealmAuthenticator = new ModularRealmAuthenticator(); modularRealmAuthenticator.setRealms(ImmutableList.of(killbillJdbcTenantRealm)); diff --git a/profiles/killbill/src/main/resources/SpyCarAdvanced.xml b/profiles/killbill/src/main/resources/SpyCarAdvanced.xml index be7c2c67cf..f02a9cdce4 100644 --- a/profiles/killbill/src/main/resources/SpyCarAdvanced.xml +++ b/profiles/killbill/src/main/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/profiles/killbill/src/main/resources/killbill-server.properties b/profiles/killbill/src/main/resources/killbill-server.properties index e418e118f4..734d17b63d 100644 --- a/profiles/killbill/src/main/resources/killbill-server.properties +++ b/profiles/killbill/src/main/resources/killbill-server.properties @@ -29,27 +29,24 @@ org.killbill.dao.logLevel=DEBUG org.killbill.catalog.uri=SpyCarAdvanced.xml # NotificationQ, Bus, ExtBus config -org.killbill.notificationq.main.sleep=1000 -org.killbill.notificationq.main.claimed=10 -org.killbill.notificationq.main.sticky=true +org.killbill.notificationq.main.sleep=100 +org.killbill.notificationq.main.claimed=1 +org.killbill.notificationq.main.queue.mode=STICKY_POLLING +org.killbill.notificationq.main.notification.nbThreads=1 -org.killbill.persistent.bus.external.sticky=true +org.killbill.persistent.bus.external.queue.mode=STICKY_EVENTS org.killbill.persistent.bus.external.inMemory=true -#org.killbill.persistent.bus.external.sticky=true +#org.killbill.persistent.bus.external.queue.mode=STICKY_EVENTS #org.killbill.persistent.bus.external.claimed=1 -#org.killbill.persistent.bus.external.inflight.claimed=1 #org.killbill.persistent.bus.external.nbThreads=1 #org.killbill.persistent.bus.external.sleep=0 -#org.killbill.persistent.bus.external.useInflightQ=true #org.killbill.persistent.bus.external.queue.capacity=100 -org.killbill.persistent.bus.main.sticky=true +org.killbill.persistent.bus.main.queue.mode=STICKY_EVENTS org.killbill.persistent.bus.main.claimed=1 -org.killbill.persistent.bus.main.inflight.claimed=1 org.killbill.persistent.bus.main.nbThreads=1 org.killbill.persistent.bus.main.sleep=0 -org.killbill.persistent.bus.main.useInflightQ=true org.killbill.persistent.bus.main.queue.capacity=100 # Start KB in multi-tenant diff --git a/profiles/killbill/src/main/resources/killbill-server.properties.mos b/profiles/killbill/src/main/resources/killbill-server.properties.mos deleted file mode 100644 index 735f895f95..0000000000 --- a/profiles/killbill/src/main/resources/killbill-server.properties.mos +++ /dev/null @@ -1,88 +0,0 @@ -# -# Copyright 2010-2013 Ning, Inc. -# Copyright 2014 Groupon, Inc -# Copyright 2014 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. -# - -# -# KILLBILL GENERIC PROPERTIES -# -# Database config -#org.killbill.dao.url=jdbc:mysql://127.0.0.1:3306/killbill -org.killbill.dao.url=jdbc:mysql://127.0.0.1:3306/killbil_analytics_mos -org.killbill.dao.user=root -org.killbill.dao.password=root -org.killbill.dao.logLevel=DEBUG - -# Use the SpyCarAdvanced.xml catalog -#org.killbill.catalog.uri=SpyCarAdvanced.xml -org.killbill.catalog.uri=CatalogMos.xml - -# NotificationQ, Bus, ExtBus config -org.killbill.notificationq.main.sleep=1000 -org.killbill.notificationq.main.claimed=10 -org.killbill.notificationq.main.sticky=true - -org.killbill.persistent.bus.external.sticky=true -org.killbill.persistent.bus.external.inMemory=true - -org.killbill.persistent.bus.main.sticky=true -org.killbill.persistent.bus.main.claimed=1 -org.killbill.persistent.bus.main.inflight.claimed=1 -org.killbill.persistent.bus.main.nbThreads=1 -org.killbill.persistent.bus.main.sleep=0 -org.killbill.persistent.bus.main.useInflightQ=true -org.killbill.persistent.bus.main.queue.capacity=100 - -# Start KB in multi-tenant -org.killbill.server.multitenant=true - -# Override polling from Tenant Broadcast Task -org.killbill.tenant.broadcast.rate=1s - -# -# PLUGIN SPECIFIC PROPERTIES -# -# Database config (OSGI plugins) -#org.killbill.billing.osgi.dao.url=jdbc:mysql://127.0.0.1:3306/killbill -org.killbill.billing.osgi.dao.url=jdbc:mysql://127.0.0.1:3306/killbil_analytics_mos -org.killbill.billing.osgi.dao.user=root -org.killbill.billing.osgi.dao.password=root - -# Allow jruby concurrency -org.killbill.jruby.context.scope=THREADSAFE - -# Path for plugin config -#org.killbill.billing.osgi.bundles.jruby.conf.dir=/var/tmp/bundles/plugins/config -org.killbill.osgi.bundle.install.dir=/var/tmp/bundles_analytics_mos - -# Config property files for plugin to access -org.killbill.server.properties=/Users/sbrossier/Src/killbill/killbill/profiles/killbill/src/main/resources/killbill-server.properties - -# -# INTEGRATION TESTS ONLY -# -# To enable test endpoint and have Kill Bill run with a ClockMock (should not be used for production server) -org.killbill.server.test.mode=true - -# Set payment calls to timeout after 5 sec -- mostly for integration tests -org.killbill.payment.plugin.timeout=5s - -org.killbill.payment.retry.days= - -org.killbill.catalog.bundlePath=CatalogTranslation -org.killbill.template.bundlePath=InvoiceTranslation -org.killbill.template.name=HtmlInvoiceTemplate.mustache - diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestChargeback.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestChargeback.java index ab4fb282b4..50e2a2d571 100644 --- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestChargeback.java +++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestChargeback.java @@ -67,12 +67,7 @@ public void testMultipleChargeback() throws Exception { } // Last attempt should fail because this is more than the Payment - try { - killBillClient.createInvoicePaymentChargeback(input, createdBy, reason, comment); - fail(); - } catch (final KillBillClientException e) { - } - + final InvoicePayment foo = killBillClient.createInvoicePaymentChargeback(input, createdBy, reason, comment); final List payments = killBillClient.getInvoicePaymentsForAccount(payment.getAccountId()); final List transactions = getPaymentTransactions(payments, TransactionType.CHARGEBACK.toString()); Assert.assertEquals(transactions.size(), 5); diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java index c48147d59f..4457006647 100644 --- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java +++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java @@ -20,11 +20,10 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; import java.util.List; import java.util.UUID; -import javax.annotation.Nullable; - import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.LocalDate; @@ -39,9 +38,9 @@ import org.killbill.billing.client.model.InvoicePayment; import org.killbill.billing.client.model.InvoicePayments; import org.killbill.billing.client.model.Invoices; -import org.killbill.billing.client.model.Payment; import org.killbill.billing.client.model.PaymentMethod; import org.killbill.billing.entitlement.api.SubscriptionEventType; +import org.killbill.billing.invoice.api.DryRunType; import org.killbill.billing.payment.provider.ExternalPaymentProviderPlugin; import org.killbill.billing.util.api.AuditLevel; import org.testng.Assert; @@ -51,7 +50,6 @@ import com.google.common.collect.Iterables; import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; @@ -94,7 +92,10 @@ public void testInvoiceOk() throws Exception { assertEquals(firstInvoiceByNumberJson, invoiceJson); // Then create a dryRun for next upcoming invoice - final Invoice dryRunInvoice = killBillClient.createDryRunInvoice(accountJson.getAccountId(), null, true, null, createdBy, reason, comment); + final InvoiceDryRun dryRunArg = new InvoiceDryRun(DryRunType.UPCOMING_INVOICE, null, + null, null, null, null, null, null, null, null, null, null); + + final Invoice dryRunInvoice = killBillClient.createDryRunInvoice(accountJson.getAccountId(), null, dryRunArg, createdBy, reason, comment); assertEquals(dryRunInvoice.getBalance(), new BigDecimal("249.95")); assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2012, 6, 25)); assertEquals(dryRunInvoice.getItems().size(), 1); @@ -111,7 +112,6 @@ public void testInvoiceOk() throws Exception { assertEquals(newInvoiceList.size(), 3); } - @Test(groups = "slow", description = "Can create a subscription in dryRun mode and get an invoice back") public void testDryRunSubscriptionCreate() throws Exception { final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0); @@ -119,9 +119,9 @@ public void testDryRunSubscriptionCreate() throws Exception { // "Assault-Rifle", BillingPeriod.ANNUAL, "rescue", BillingActionPolicy.IMMEDIATE, final Account accountJson = createAccountWithDefaultPaymentMethod(); - final InvoiceDryRun dryRunArg = new InvoiceDryRun(SubscriptionEventType.START_BILLING, + final InvoiceDryRun dryRunArg = new InvoiceDryRun(DryRunType.TARGET_DATE, SubscriptionEventType.START_BILLING, null, "Assault-Rifle", ProductCategory.BASE, BillingPeriod.ANNUAL, null, null, null, null, null, null); - final Invoice dryRunInvoice = killBillClient.createDryRunInvoice(accountJson.getAccountId(), new LocalDate(initialDate, DateTimeZone.forID(accountJson.getTimeZone())), false, dryRunArg, createdBy, reason, comment); + final Invoice dryRunInvoice = killBillClient.createDryRunInvoice(accountJson.getAccountId(), new LocalDate(initialDate, DateTimeZone.forID(accountJson.getTimeZone())), dryRunArg, createdBy, reason, comment); assertEquals(dryRunInvoice.getItems().size(), 1); } @@ -351,6 +351,41 @@ public void testExternalChargeOnNewInvoice() throws Exception { assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId()).size(), 3); } + @Test(groups = "slow", description = "Can create multiple external charges") + public void testExternalCharges() throws Exception { + final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice(); + + // Get the invoices + assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId()).size(), 2); + + // Post an external charge + final BigDecimal chargeAmount = BigDecimal.TEN; + + final List externalCharges = new ArrayList(); + + // Does not pass currency to test on purpose that we will default to account currency + final InvoiceItem externalCharge1 = new InvoiceItem(); + externalCharge1.setAccountId(accountJson.getAccountId()); + externalCharge1.setAmount(chargeAmount); + externalCharge1.setDescription(UUID.randomUUID().toString()); + externalCharges.add(externalCharge1); + + final InvoiceItem externalCharge2 = new InvoiceItem(); + externalCharge2.setAccountId(accountJson.getAccountId()); + externalCharge2.setAmount(chargeAmount); + externalCharge2.setCurrency(Currency.valueOf(accountJson.getCurrency())); + externalCharge2.setDescription(UUID.randomUUID().toString()); + externalCharges.add(externalCharge2); + + final List createdExternalCharges = killBillClient.createExternalCharges(externalCharges, clock.getUTCNow(), false, createdBy, reason, comment); + assertEquals(createdExternalCharges.size(), 2); + assertEquals(createdExternalCharges.get(0).getCurrency().toString(), accountJson.getCurrency()); + assertEquals(createdExternalCharges.get(1).getCurrency().toString(), accountJson.getCurrency()); + + // Verify the total number of invoices + assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId()).size(), 3); + } + @Test(groups = "slow", description = "Can create an external charge and trigger a payment") public void testExternalChargeOnNewInvoiceWithAutomaticPayment() throws Exception { final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice(); diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoicePayment.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoicePayment.java index 880b81c3bf..bf11eaab0d 100644 --- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoicePayment.java +++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoicePayment.java @@ -96,6 +96,9 @@ public void testFullRefundWithInvoiceAdjustment() throws Exception { final BigDecimal refundAmount = paymentJson.getPurchasedAmount(); final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO; + final InvoicePayments invoicePayments = killBillClient.getInvoicePayment(paymentJson.getTargetInvoiceId()); + Assert.assertEquals(invoicePayments.size(), 1); + // Post and verify the refund final InvoicePaymentTransaction refund = new InvoicePaymentTransaction(); refund.setPaymentId(paymentJson.getPaymentId()); @@ -106,6 +109,10 @@ public void testFullRefundWithInvoiceAdjustment() throws Exception { // Verify the invoice balance verifyInvoice(paymentJson, expectedInvoiceBalance); + + final InvoicePayments invoicePaymentsAfterRefund = killBillClient.getInvoicePayment(paymentJson.getTargetInvoiceId()); + Assert.assertEquals(invoicePaymentsAfterRefund.size(), 1); + } @Test(groups = "slow", description = "Can create a partial refund with invoice adjustment") diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java index bdfaa5d1e7..fa484200f7 100644 --- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java +++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java @@ -54,6 +54,7 @@ import org.killbill.billing.server.modules.KillbillServerModule; import org.killbill.billing.util.cache.CacheControllerDispatcher; import org.killbill.billing.util.config.PaymentConfig; +import org.killbill.billing.util.config.SecurityConfig; import org.killbill.bus.api.PersistentBus; import org.killbill.commons.jdbi.guice.DaoConfig; import org.skife.config.ConfigurationObjectFactory; @@ -96,6 +97,9 @@ public class TestJaxrsBase extends KillbillClient { @Named(KillbillServerModule.SHIRO_DATA_SOURCE_ID) protected DataSource shiroDataSource; + @Inject + protected SecurityConfig securityConfig; + protected DaoConfig daoConfig; protected KillbillServerConfig serverConfig; diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java index 3aab61aee0..09d899a00b 100644 --- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java +++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java @@ -1,6 +1,6 @@ /* - * 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.jaxrs; import java.math.BigDecimal; +import java.util.Map; import java.util.UUID; import javax.annotation.Nullable; @@ -31,16 +32,51 @@ import org.killbill.billing.client.model.PaymentTransaction; import org.killbill.billing.client.model.Payments; import org.killbill.billing.client.model.PluginProperty; -import org.killbill.billing.jaxrs.json.PluginPropertyJson; +import org.killbill.billing.osgi.api.OSGIServiceRegistration; +import org.killbill.billing.payment.api.TransactionType; +import org.killbill.billing.payment.plugin.api.PaymentPluginApi; +import org.killbill.billing.payment.plugin.api.PaymentPluginStatus; +import org.killbill.billing.payment.provider.MockPaymentProviderPlugin; import org.testng.Assert; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import com.google.common.base.Objects; +import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.inject.Inject; + +import static org.testng.Assert.assertEquals; public class TestPayment extends TestJaxrsBase { + @Inject + protected OSGIServiceRegistration registry; + + private MockPaymentProviderPlugin mockPaymentProviderPlugin; + + @BeforeMethod(groups = "slow") + public void beforeMethod() throws Exception { + super.beforeMethod(); + mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(PLUGIN_NAME); + mockPaymentProviderPlugin.clear(); + } + + @Test(groups = "slow") + public void testWithFailedPayment() throws Exception { + final Account account = createAccountWithDefaultPaymentMethod(); + + mockPaymentProviderPlugin.makeNextPaymentFailWithError(); + + final PaymentTransaction authTransaction = new PaymentTransaction(); + authTransaction.setAmount(BigDecimal.ONE); + authTransaction.setCurrency(account.getCurrency()); + authTransaction.setTransactionType(TransactionType.AUTHORIZE.name()); + final Payment payment = killBillClient.createPayment(account.getAccountId(), account.getPaymentMethodId(), authTransaction, ImmutableMap.of(), createdBy, reason, comment); + assertEquals(payment.getTransactions().get(0).getGatewayErrorCode(), MockPaymentProviderPlugin.GATEWAY_ERROR_CODE); + assertEquals(payment.getTransactions().get(0).getGatewayErrorMsg(), MockPaymentProviderPlugin.GATEWAY_ERROR); + } + @Test(groups = "slow") public void testCreateRetrievePayment() throws Exception { final Account account = createAccountWithDefaultPaymentMethod(); @@ -51,6 +87,113 @@ public void testCreateRetrievePayment() throws Exception { testCreateRetrievePayment(account, nonDefaultPaymentMethod.getPaymentMethodId(), UUID.randomUUID().toString(), 2); } + @Test(groups = "slow") + public void testCompletionForInitialTransaction() throws Exception { + final Account account = createAccountWithDefaultPaymentMethod(); + final UUID paymentMethodId = account.getPaymentMethodId(); + final BigDecimal amount = BigDecimal.TEN; + + final String pending = PaymentPluginStatus.PENDING.toString(); + final ImmutableMap pluginProperties = ImmutableMap.of(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, pending); + + int paymentNb = 0; + for (final TransactionType transactionType : ImmutableList.of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) { + final BigDecimal authAmount = BigDecimal.ZERO; + final String paymentExternalKey = UUID.randomUUID().toString(); + final String authTransactionExternalKey = UUID.randomUUID().toString(); + paymentNb++; + + final Payment initialPayment = createVerifyTransaction(account, paymentMethodId, paymentExternalKey, authTransactionExternalKey, transactionType, pending, amount, authAmount, pluginProperties, paymentNb); + final PaymentTransaction authPaymentTransaction = initialPayment.getTransactions().get(0); + + // Complete operation: first, only specify the payment id + final PaymentTransaction completeTransactionByPaymentId = new PaymentTransaction(); + completeTransactionByPaymentId.setPaymentId(initialPayment.getPaymentId()); + final Payment completedPaymentByPaymentId = killBillClient.completePayment(completeTransactionByPaymentId, pluginProperties, createdBy, reason, comment); + verifyPayment(account, paymentMethodId, completedPaymentByPaymentId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), pending, amount, authAmount, BigDecimal.ZERO, BigDecimal.ZERO, 1, paymentNb); + + // Second, only specify the payment external key + final PaymentTransaction completeTransactionByPaymentExternalKey = new PaymentTransaction(); + completeTransactionByPaymentExternalKey.setPaymentExternalKey(initialPayment.getPaymentExternalKey()); + final Payment completedPaymentByExternalKey = killBillClient.completePayment(completeTransactionByPaymentExternalKey, pluginProperties, createdBy, reason, comment); + verifyPayment(account, paymentMethodId, completedPaymentByExternalKey, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), pending, amount, authAmount, BigDecimal.ZERO, BigDecimal.ZERO, 1, paymentNb); + + // Third, specify the payment id and transaction external key + final PaymentTransaction completeTransactionWithTypeAndKey = new PaymentTransaction(); + completeTransactionWithTypeAndKey.setPaymentId(initialPayment.getPaymentId()); + completeTransactionWithTypeAndKey.setTransactionExternalKey(authPaymentTransaction.getTransactionExternalKey()); + final Payment completedPaymentByTypeAndKey = killBillClient.completePayment(completeTransactionWithTypeAndKey, pluginProperties, createdBy, reason, comment); + verifyPayment(account, paymentMethodId, completedPaymentByTypeAndKey, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), pending, amount, authAmount, BigDecimal.ZERO, BigDecimal.ZERO, 1, paymentNb); + + // Finally, specify the payment id and transaction id + final PaymentTransaction completeTransactionWithTypeAndId = new PaymentTransaction(); + completeTransactionWithTypeAndId.setPaymentId(initialPayment.getPaymentId()); + completeTransactionWithTypeAndId.setTransactionId(authPaymentTransaction.getTransactionId()); + final Payment completedPaymentByTypeAndId = killBillClient.completePayment(completeTransactionWithTypeAndId, pluginProperties, createdBy, reason, comment); + verifyPayment(account, paymentMethodId, completedPaymentByTypeAndId, paymentExternalKey, authTransactionExternalKey, transactionType.toString(), pending, amount, authAmount, BigDecimal.ZERO, BigDecimal.ZERO, 1, paymentNb); + } + } + + @Test(groups = "slow") + public void testCompletionForSubsequentTransaction() throws Exception { + final Account account = createAccountWithDefaultPaymentMethod(); + final UUID paymentMethodId = account.getPaymentMethodId(); + final String paymentExternalKey = UUID.randomUUID().toString(); + final String purchaseTransactionExternalKey = UUID.randomUUID().toString(); + final BigDecimal purchaseAmount = BigDecimal.TEN; + + // Create a successful purchase + final Payment authPayment = createVerifyTransaction(account, paymentMethodId, paymentExternalKey, purchaseTransactionExternalKey, TransactionType.PURCHASE, + "SUCCESS", purchaseAmount, BigDecimal.ZERO, ImmutableMap.of(), 1); + + final String pending = PaymentPluginStatus.PENDING.toString(); + final ImmutableMap pluginProperties = ImmutableMap.of(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, pending); + + // Trigger a pending refund + final String refundTransactionExternalKey = UUID.randomUUID().toString(); + final PaymentTransaction refundTransaction = new PaymentTransaction(); + refundTransaction.setPaymentId(authPayment.getPaymentId()); + refundTransaction.setTransactionExternalKey(refundTransactionExternalKey); + refundTransaction.setAmount(purchaseAmount); + refundTransaction.setCurrency(authPayment.getCurrency()); + final Payment refundPayment = killBillClient.refundPayment(refundTransaction, pluginProperties, createdBy, reason, comment); + verifyPaymentWithPendingRefund(account, paymentMethodId, paymentExternalKey, purchaseTransactionExternalKey, purchaseAmount, refundTransactionExternalKey, refundPayment); + + // We cannot complete using just the payment id as JAX-RS doesn't know which transaction to complete + try { + final PaymentTransaction completeTransactionByPaymentId = new PaymentTransaction(); + completeTransactionByPaymentId.setPaymentId(refundPayment.getPaymentId()); + killBillClient.completePayment(completeTransactionByPaymentId, pluginProperties, createdBy, reason, comment); + Assert.fail(); + } catch (final KillBillClientException e) { + assertEquals(e.getMessage(), "PaymentTransactionJson transactionType and externalKey need to be set"); + } + + // We cannot complete using just the payment external key as JAX-RS doesn't know which transaction to complete + try { + final PaymentTransaction completeTransactionByPaymentExternalKey = new PaymentTransaction(); + completeTransactionByPaymentExternalKey.setPaymentExternalKey(refundPayment.getPaymentExternalKey()); + killBillClient.completePayment(completeTransactionByPaymentExternalKey, pluginProperties, createdBy, reason, comment); + Assert.fail(); + } catch (final KillBillClientException e) { + assertEquals(e.getMessage(), "PaymentTransactionJson transactionType and externalKey need to be set"); + } + + // Finally, it should work if we specify the payment id and transaction external key + final PaymentTransaction completeTransactionWithTypeAndKey = new PaymentTransaction(); + completeTransactionWithTypeAndKey.setPaymentId(refundPayment.getPaymentId()); + completeTransactionWithTypeAndKey.setTransactionExternalKey(refundTransactionExternalKey); + final Payment completedPaymentByTypeAndKey = killBillClient.completePayment(completeTransactionWithTypeAndKey, pluginProperties, createdBy, reason, comment); + verifyPaymentWithPendingRefund(account, paymentMethodId, paymentExternalKey, purchaseTransactionExternalKey, purchaseAmount, refundTransactionExternalKey, completedPaymentByTypeAndKey); + + // Also, it should work if we specify the payment id and transaction id + final PaymentTransaction completeTransactionWithTypeAndId = new PaymentTransaction(); + completeTransactionWithTypeAndId.setPaymentId(refundPayment.getPaymentId()); + completeTransactionWithTypeAndId.setTransactionId(refundPayment.getTransactions().get(1).getTransactionId()); + final Payment completedPaymentByTypeAndId = killBillClient.completePayment(completeTransactionWithTypeAndId, pluginProperties, createdBy, reason, comment); + verifyPaymentWithPendingRefund(account, paymentMethodId, paymentExternalKey, purchaseTransactionExternalKey, purchaseAmount, refundTransactionExternalKey, completedPaymentByTypeAndId); + } + @Test(groups = "slow") public void testComboAuthorization() throws Exception { final Account accountJson = getAccount(); @@ -74,33 +217,39 @@ public void testComboAuthorization() throws Exception { final ComboPaymentTransaction comboPaymentTransaction = new ComboPaymentTransaction(accountJson, paymentMethodJson, authTransactionJson, ImmutableList.of(), ImmutableList.of()); final Payment payment = killBillClient.createPayment(comboPaymentTransaction, ImmutableMap.of(), createdBy, reason, comment); - verifyComboPayment(payment, paymentExternalKey, - BigDecimal.TEN, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1); - + verifyComboPayment(payment, paymentExternalKey, BigDecimal.TEN, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1); // Void payment using externalKey final String voidTransactionExternalKey = UUID.randomUUID().toString(); final Payment voidPayment = killBillClient.voidPayment(null, paymentExternalKey, voidTransactionExternalKey, ImmutableMap.of(), createdBy, reason, comment); - verifyPaymentTransaction(voidPayment.getPaymentId(), voidPayment.getTransactions().get(1), - paymentExternalKey, voidTransactionExternalKey, - accountJson, null, "VOID"); + verifyPaymentTransaction(accountJson, voidPayment.getPaymentId(), paymentExternalKey, voidPayment.getTransactions().get(1), + voidTransactionExternalKey, null, "VOID", "SUCCESS"); + } + + @Test(groups = "slow") + public void testComboAuthorizationInvalidPaymentMethod() throws Exception { + final Account accountJson = getAccount(); + accountJson.setAccountId(null); + + final PaymentMethodPluginDetail info = new PaymentMethodPluginDetail(); + info.setProperties(null); + final UUID paymentMethodId = UUID.randomUUID(); + final PaymentMethod paymentMethodJson = new PaymentMethod(paymentMethodId, null, null, true, PLUGIN_NAME, info); + final ComboPaymentTransaction comboPaymentTransaction = new ComboPaymentTransaction(accountJson, paymentMethodJson, null, ImmutableList.of(), ImmutableList.of()); + + final Payment payment = killBillClient.createPayment(comboPaymentTransaction, ImmutableMap.of(), createdBy, reason, comment); + // Client returns null in case of a 404 + Assert.assertNull(payment); } private void testCreateRetrievePayment(final Account account, @Nullable final UUID paymentMethodId, - final String PaymentExternalKey, final int PaymentNb) throws Exception { + final String paymentExternalKey, final int paymentNb) throws Exception { // Authorization final String authTransactionExternalKey = UUID.randomUUID().toString(); - final PaymentTransaction authTransaction = new PaymentTransaction(); - authTransaction.setAmount(BigDecimal.TEN); - authTransaction.setCurrency(account.getCurrency()); - authTransaction.setPaymentExternalKey(PaymentExternalKey); - authTransaction.setTransactionExternalKey(authTransactionExternalKey); - authTransaction.setTransactionType("AUTHORIZE"); - final Payment authPayment = killBillClient.createPayment(account.getAccountId(), paymentMethodId, authTransaction, createdBy, reason, comment); - verifyPayment(account, paymentMethodId, authPayment, PaymentExternalKey, authTransactionExternalKey, - BigDecimal.TEN, BigDecimal.ZERO, BigDecimal.ZERO, 1, PaymentNb); + final Payment authPayment = createVerifyTransaction(account, paymentMethodId, paymentExternalKey, authTransactionExternalKey, TransactionType.AUTHORIZE, + "SUCCESS", BigDecimal.TEN, BigDecimal.TEN, ImmutableMap.of(), paymentNb); // Capture 1 final String capture1TransactionExternalKey = UUID.randomUUID().toString(); @@ -108,15 +257,14 @@ private void testCreateRetrievePayment(final Account account, @Nullable final UU captureTransaction.setPaymentId(authPayment.getPaymentId()); captureTransaction.setAmount(BigDecimal.ONE); captureTransaction.setCurrency(account.getCurrency()); - captureTransaction.setPaymentExternalKey(PaymentExternalKey); + captureTransaction.setPaymentExternalKey(paymentExternalKey); captureTransaction.setTransactionExternalKey(capture1TransactionExternalKey); // captureAuthorization is using paymentId final Payment capturedPayment1 = killBillClient.captureAuthorization(captureTransaction, createdBy, reason, comment); - verifyPayment(account, paymentMethodId, capturedPayment1, PaymentExternalKey, authTransactionExternalKey, - BigDecimal.TEN, BigDecimal.ONE, BigDecimal.ZERO, 2, PaymentNb); - verifyPaymentTransaction(authPayment.getPaymentId(), capturedPayment1.getTransactions().get(1), - PaymentExternalKey, capture1TransactionExternalKey, - account, captureTransaction.getAmount(), "CAPTURE"); + verifyPayment(account, paymentMethodId, capturedPayment1, paymentExternalKey, authTransactionExternalKey, "AUTHORIZE", "SUCCESS", + BigDecimal.TEN, BigDecimal.TEN, BigDecimal.ONE, BigDecimal.ZERO, 2, paymentNb); + verifyPaymentTransaction(account, authPayment.getPaymentId(), paymentExternalKey, capturedPayment1.getTransactions().get(1), + capture1TransactionExternalKey, captureTransaction.getAmount(), "CAPTURE", "SUCCESS"); // Capture 2 final String capture2TransactionExternalKey = UUID.randomUUID().toString(); @@ -124,11 +272,10 @@ private void testCreateRetrievePayment(final Account account, @Nullable final UU // captureAuthorization is using externalKey captureTransaction.setPaymentId(null); final Payment capturedPayment2 = killBillClient.captureAuthorization(captureTransaction, createdBy, reason, comment); - verifyPayment(account, paymentMethodId, capturedPayment2, PaymentExternalKey, authTransactionExternalKey, - BigDecimal.TEN, new BigDecimal("2"), BigDecimal.ZERO, 3, PaymentNb); - verifyPaymentTransaction(authPayment.getPaymentId(), capturedPayment2.getTransactions().get(2), - PaymentExternalKey, capture2TransactionExternalKey, - account, captureTransaction.getAmount(), "CAPTURE"); + verifyPayment(account, paymentMethodId, capturedPayment2, paymentExternalKey, authTransactionExternalKey, "AUTHORIZE", "SUCCESS", + BigDecimal.TEN, BigDecimal.TEN, new BigDecimal("2"), BigDecimal.ZERO, 3, paymentNb); + verifyPaymentTransaction(account, authPayment.getPaymentId(), paymentExternalKey, capturedPayment2.getTransactions().get(2), + capture2TransactionExternalKey, captureTransaction.getAmount(), "CAPTURE", "SUCCESS"); // Refund final String refundTransactionExternalKey = UUID.randomUUID().toString(); @@ -136,44 +283,36 @@ private void testCreateRetrievePayment(final Account account, @Nullable final UU refundTransaction.setPaymentId(authPayment.getPaymentId()); refundTransaction.setAmount(new BigDecimal("2")); refundTransaction.setCurrency(account.getCurrency()); - refundTransaction.setPaymentExternalKey(PaymentExternalKey); + refundTransaction.setPaymentExternalKey(paymentExternalKey); refundTransaction.setTransactionExternalKey(refundTransactionExternalKey); final Payment refundPayment = killBillClient.refundPayment(refundTransaction, createdBy, reason, comment); - verifyPayment(account, paymentMethodId, refundPayment, PaymentExternalKey, authTransactionExternalKey, - BigDecimal.TEN, new BigDecimal("2"), new BigDecimal("2"), 4, PaymentNb); - verifyPaymentTransaction(authPayment.getPaymentId(), refundPayment.getTransactions().get(3), - PaymentExternalKey, refundTransactionExternalKey, - account, refundTransaction.getAmount(), "REFUND"); + verifyPayment(account, paymentMethodId, refundPayment, paymentExternalKey, authTransactionExternalKey, "AUTHORIZE", "SUCCESS", + BigDecimal.TEN, BigDecimal.TEN, new BigDecimal("2"), new BigDecimal("2"), 4, paymentNb); + verifyPaymentTransaction(account, authPayment.getPaymentId(), paymentExternalKey, refundPayment.getTransactions().get(3), + refundTransactionExternalKey, refundTransaction.getAmount(), "REFUND", "SUCCESS"); } - private void verifyPayment(final Account account, @Nullable final UUID paymentMethodId, final Payment Payment, - final String PaymentExternalKey, final String authTransactionExternalKey, - final BigDecimal authAmount, final BigDecimal capturedAmount, - final BigDecimal refundedAmount, final int nbTransactions, final int PaymentNb) throws KillBillClientException { - Assert.assertEquals(Payment.getAccountId(), account.getAccountId()); - Assert.assertEquals(Payment.getPaymentMethodId(), Objects.firstNonNull(paymentMethodId, account.getPaymentMethodId())); - Assert.assertNotNull(Payment.getPaymentId()); - Assert.assertNotNull(Payment.getPaymentNumber()); - Assert.assertEquals(Payment.getPaymentExternalKey(), PaymentExternalKey); - Assert.assertEquals(Payment.getAuthAmount().compareTo(authAmount), 0); - Assert.assertEquals(Payment.getCapturedAmount().compareTo(capturedAmount), 0); - Assert.assertEquals(Payment.getRefundedAmount().compareTo(refundedAmount), 0); - Assert.assertEquals(Payment.getCurrency(), account.getCurrency()); - Assert.assertEquals(Payment.getTransactions().size(), nbTransactions); - - verifyPaymentTransaction(Payment.getPaymentId(), Payment.getTransactions().get(0), - PaymentExternalKey, authTransactionExternalKey, account, authAmount, "AUTHORIZE"); - - final Payments Payments = killBillClient.getPayments(); - Assert.assertEquals(Payments.size(), PaymentNb); - Assert.assertEquals(Payments.get(PaymentNb - 1), Payment); + private Payment createVerifyTransaction(final Account account, + @Nullable final UUID paymentMethodId, + final String paymentExternalKey, + final String transactionExternalKey, + final TransactionType transactionType, + final String transactionStatus, + final BigDecimal transactionAmount, + final BigDecimal authAmount, + final Map pluginProperties, + final int paymentNb) throws KillBillClientException { + final PaymentTransaction authTransaction = new PaymentTransaction(); + authTransaction.setAmount(transactionAmount); + authTransaction.setCurrency(account.getCurrency()); + authTransaction.setPaymentExternalKey(paymentExternalKey); + authTransaction.setTransactionExternalKey(transactionExternalKey); + authTransaction.setTransactionType(transactionType.toString()); + final Payment payment = killBillClient.createPayment(account.getAccountId(), paymentMethodId, authTransaction, pluginProperties, createdBy, reason, comment); - final Payment retrievedPayment = killBillClient.getPayment(Payment.getPaymentId()); - Assert.assertEquals(retrievedPayment, Payment); + verifyPayment(account, paymentMethodId, payment, paymentExternalKey, transactionExternalKey, transactionType.toString(), transactionStatus, transactionAmount, authAmount, BigDecimal.ZERO, BigDecimal.ZERO, 1, paymentNb); - final Payments paymentsForAccount = killBillClient.getPaymentsForAccount(account.getAccountId()); - Assert.assertEquals(paymentsForAccount.size(), PaymentNb); - Assert.assertEquals(paymentsForAccount.get(PaymentNb - 1), Payment); + return payment; } private void verifyComboPayment(final Payment payment, @@ -182,36 +321,93 @@ private void verifyComboPayment(final Payment payment, final BigDecimal capturedAmount, final BigDecimal refundedAmount, final int nbTransactions, - final int PaymentNb) throws KillBillClientException { + final int paymentNb) throws KillBillClientException { + Assert.assertNotNull(payment.getPaymentNumber()); + assertEquals(payment.getPaymentExternalKey(), paymentExternalKey); + assertEquals(payment.getAuthAmount().compareTo(authAmount), 0); + assertEquals(payment.getCapturedAmount().compareTo(capturedAmount), 0); + assertEquals(payment.getRefundedAmount().compareTo(refundedAmount), 0); + assertEquals(payment.getTransactions().size(), nbTransactions); + final Payments Payments = killBillClient.getPayments(); + assertEquals(Payments.size(), paymentNb); + assertEquals(Payments.get(paymentNb - 1), payment); + } + + private void verifyPayment(final Account account, + @Nullable final UUID paymentMethodId, + final Payment payment, + final String paymentExternalKey, + final String firstTransactionExternalKey, + final String firstTransactionType, + final String firstTransactionStatus, + final BigDecimal firstTransactionAmount, + final BigDecimal paymentAuthAmount, + final BigDecimal capturedAmount, + final BigDecimal refundedAmount, + final int nbTransactions, + final int paymentNb) throws KillBillClientException { + verifyPaymentNoTransaction(account, paymentMethodId, payment, paymentExternalKey, paymentAuthAmount, capturedAmount, refundedAmount, nbTransactions, paymentNb); + verifyPaymentTransaction(account, payment.getPaymentId(), paymentExternalKey, payment.getTransactions().get(0), firstTransactionExternalKey, firstTransactionAmount, firstTransactionType, firstTransactionStatus); + } + + private void verifyPaymentNoTransaction(final Account account, + @Nullable final UUID paymentMethodId, + final Payment payment, + final String paymentExternalKey, + final BigDecimal authAmount, + final BigDecimal capturedAmount, + final BigDecimal refundedAmount, + final int nbTransactions, + final int paymentNb) throws KillBillClientException { + assertEquals(payment.getAccountId(), account.getAccountId()); + assertEquals(payment.getPaymentMethodId(), MoreObjects.firstNonNull(paymentMethodId, account.getPaymentMethodId())); + Assert.assertNotNull(payment.getPaymentId()); Assert.assertNotNull(payment.getPaymentNumber()); - Assert.assertEquals(payment.getPaymentExternalKey(), paymentExternalKey); - Assert.assertEquals(payment.getAuthAmount().compareTo(authAmount), 0); - Assert.assertEquals(payment.getCapturedAmount().compareTo(capturedAmount), 0); - Assert.assertEquals(payment.getRefundedAmount().compareTo(refundedAmount), 0); - Assert.assertEquals(payment.getTransactions().size(), nbTransactions); + assertEquals(payment.getPaymentExternalKey(), paymentExternalKey); + assertEquals(payment.getAuthAmount().compareTo(authAmount), 0); + assertEquals(payment.getCapturedAmount().compareTo(capturedAmount), 0); + assertEquals(payment.getRefundedAmount().compareTo(refundedAmount), 0); + assertEquals(payment.getCurrency(), account.getCurrency()); + assertEquals(payment.getTransactions().size(), nbTransactions); final Payments Payments = killBillClient.getPayments(); - Assert.assertEquals(Payments.size(), PaymentNb); - Assert.assertEquals(Payments.get(PaymentNb - 1), payment); + assertEquals(Payments.size(), paymentNb); + assertEquals(Payments.get(paymentNb - 1), payment); + + final Payment retrievedPayment = killBillClient.getPayment(payment.getPaymentId()); + assertEquals(retrievedPayment, payment); + final Payments paymentsForAccount = killBillClient.getPaymentsForAccount(account.getAccountId()); + assertEquals(paymentsForAccount.size(), paymentNb); + assertEquals(paymentsForAccount.get(paymentNb - 1), payment); } - private void verifyPaymentTransaction(final UUID PaymentId, final PaymentTransaction PaymentTransaction, - final String PaymentExternalKey, final String TransactionExternalKey, - final Account account, @Nullable final BigDecimal amount, final String transactionType) { - Assert.assertEquals(PaymentTransaction.getPaymentId(), PaymentId); - Assert.assertNotNull(PaymentTransaction.getTransactionId()); - Assert.assertEquals(PaymentTransaction.getTransactionType(), transactionType); - Assert.assertEquals(PaymentTransaction.getStatus(), "SUCCESS"); + private void verifyPaymentTransaction(final Account account, + final UUID paymentId, + final String paymentExternalKey, + final PaymentTransaction paymentTransaction, + final String transactionExternalKey, + @Nullable final BigDecimal amount, + final String transactionType, + final String transactionStatus) { + assertEquals(paymentTransaction.getPaymentId(), paymentId); + Assert.assertNotNull(paymentTransaction.getTransactionId()); + assertEquals(paymentTransaction.getTransactionType(), transactionType); + assertEquals(paymentTransaction.getStatus(), transactionStatus); if (amount == null) { - Assert.assertNull(PaymentTransaction.getAmount()); - Assert.assertNull(PaymentTransaction.getCurrency()); + Assert.assertNull(paymentTransaction.getAmount()); + Assert.assertNull(paymentTransaction.getCurrency()); } else { - Assert.assertEquals(PaymentTransaction.getAmount().compareTo(amount), 0); - Assert.assertEquals(PaymentTransaction.getCurrency(), account.getCurrency()); + assertEquals(paymentTransaction.getAmount().compareTo(amount), 0); + assertEquals(paymentTransaction.getCurrency(), account.getCurrency()); } - Assert.assertEquals(PaymentTransaction.getTransactionExternalKey(), TransactionExternalKey); - Assert.assertEquals(PaymentTransaction.getPaymentExternalKey(), PaymentExternalKey); + assertEquals(paymentTransaction.getTransactionExternalKey(), transactionExternalKey); + assertEquals(paymentTransaction.getPaymentExternalKey(), paymentExternalKey); + } + + private void verifyPaymentWithPendingRefund(final Account account, final UUID paymentMethodId, final String paymentExternalKey, final String authTransactionExternalKey, final BigDecimal purchaseAmount, final String refundTransactionExternalKey, final Payment refundPayment) throws KillBillClientException { + verifyPayment(account, paymentMethodId, refundPayment, paymentExternalKey, authTransactionExternalKey, "PURCHASE", "SUCCESS", purchaseAmount, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, 2, 1); + verifyPaymentTransaction(account, refundPayment.getPaymentId(), paymentExternalKey, refundPayment.getTransactions().get(1), refundTransactionExternalKey, purchaseAmount, "REFUND", "PENDING"); } } diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPaymentGateway.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPaymentGateway.java new file mode 100644 index 0000000000..8da0baa099 --- /dev/null +++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPaymentGateway.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Groupon, Inc + * Copyright 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.jaxrs; + +import java.util.UUID; + +import org.killbill.billing.client.model.Account; +import org.killbill.billing.client.model.ComboHostedPaymentPage; +import org.killbill.billing.client.model.HostedPaymentPageFields; +import org.killbill.billing.client.model.HostedPaymentPageFormDescriptor; +import org.killbill.billing.client.model.PaymentMethod; +import org.killbill.billing.client.model.PaymentMethodPluginDetail; +import org.killbill.billing.client.model.PluginProperty; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.ning.http.client.Response; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +public class TestPaymentGateway extends TestJaxrsBase { + + @Test(groups = "slow") + public void testBuildFormDescriptor() throws Exception { + final Account account = createAccountWithDefaultPaymentMethod(); + + final HostedPaymentPageFields hppFields = new HostedPaymentPageFields(); + + final HostedPaymentPageFormDescriptor hostedPaymentPageFormDescriptor = killBillClient.buildFormDescriptor(hppFields, account.getAccountId(), null, ImmutableMap.of(), createdBy, reason, comment); + Assert.assertEquals(hostedPaymentPageFormDescriptor.getKbAccountId(), account.getAccountId()); + } + + @Test(groups = "slow") + public void testComboBuildFormDescriptor() throws Exception { + final Account account = getAccount(); + account.setAccountId(null); + + final PaymentMethodPluginDetail info = new PaymentMethodPluginDetail(); + final PaymentMethod paymentMethod = new PaymentMethod(null, UUID.randomUUID().toString(), null, true, PLUGIN_NAME, info); + + final HostedPaymentPageFields hppFields = new HostedPaymentPageFields(); + + final ComboHostedPaymentPage comboHostedPaymentPage = new ComboHostedPaymentPage(account, paymentMethod, ImmutableList.of(), hppFields); + + final HostedPaymentPageFormDescriptor hostedPaymentPageFormDescriptor = killBillClient.buildFormDescriptor(comboHostedPaymentPage, ImmutableMap.of(), createdBy, reason, comment); + Assert.assertNotNull(hostedPaymentPageFormDescriptor.getKbAccountId()); + } + + @Test(groups = "slow") + public void testProcessNotification() throws Exception { + final Response response = killBillClient.processNotification("TOTO", PLUGIN_NAME, ImmutableMap.of(), createdBy, reason, comment); + Assert.assertEquals(response.getStatusCode(), 200); + } +} diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTag.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTag.java index a2641298d1..725ddac408 100644 --- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTag.java +++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTag.java @@ -23,12 +23,17 @@ import javax.annotation.Nullable; +import org.joda.time.DateTime; import org.killbill.billing.ObjectType; +import org.killbill.billing.catalog.api.BillingPeriod; +import org.killbill.billing.catalog.api.ProductCategory; import org.killbill.billing.client.KillBillClientException; import org.killbill.billing.client.model.Account; +import org.killbill.billing.client.model.Subscription; import org.killbill.billing.client.model.Tag; import org.killbill.billing.client.model.TagDefinition; import org.killbill.billing.client.model.Tags; +import org.killbill.billing.util.api.AuditLevel; import org.killbill.billing.util.tag.ControlTagType; import org.testng.Assert; import org.testng.annotations.Test; @@ -94,6 +99,39 @@ public void testMultipleTagDefinitionOk() throws Exception { assertEquals(objFromJson.size(), 3 + sizeSystemTag); } + + @Test(groups = "slow", description = "Can search all tags for an account") + public void testGetAllTagsByType() throws Exception { + + final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0); + clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis()); + + final Account account = createAccountWithDefaultPaymentMethod(); + + final Subscription subscriptionJson = createEntitlement(account.getAccountId(), "87544332", "Shotgun", + ProductCategory.BASE, BillingPeriod.MONTHLY, true); + + for (final ControlTagType controlTagType : ControlTagType.values()) { + killBillClient.createAccountTag(account.getAccountId(), controlTagType.getId(), createdBy, reason, comment); + } + + final TagDefinition bundleTagDefInput = new TagDefinition(null, false, "bundleTagDef", "nothing special", ImmutableList.of()); + final TagDefinition bundleTagDef = killBillClient.createTagDefinition(bundleTagDefInput, createdBy, reason, comment); + + killBillClient.createBundleTag(subscriptionJson.getBundleId(), bundleTagDef.getId(), createdBy, reason, comment); + + final Tags allBundleTags = killBillClient.getBundleTags(subscriptionJson.getBundleId(), AuditLevel.FULL); + Assert.assertEquals(allBundleTags.size(), 1); + + final Tags allAccountTags = killBillClient.getAllAccountTags(account.getAccountId(), null, AuditLevel.FULL); + Assert.assertEquals(allAccountTags.size(), ControlTagType.values().length + 1); + + + final Tags allBundleTagsForAccount = killBillClient.getAllAccountTags(account.getAccountId(), ObjectType.BUNDLE.name(), AuditLevel.FULL); + Assert.assertEquals(allBundleTagsForAccount.size(), 1); + } + + @Test(groups = "slow", description = "Can search system tags") public void testSystemTagsPagination() throws Exception { final Account account = createAccount(); diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/security/TestKillbillJdbcTenantRealm.java b/profiles/killbill/src/test/java/org/killbill/billing/server/security/TestKillbillJdbcTenantRealm.java index b3899fdd52..a82fe1ee2e 100644 --- a/profiles/killbill/src/test/java/org/killbill/billing/server/security/TestKillbillJdbcTenantRealm.java +++ b/profiles/killbill/src/test/java/org/killbill/billing/server/security/TestKillbillJdbcTenantRealm.java @@ -49,7 +49,7 @@ public void beforeMethod() throws Exception { super.beforeMethod(); // Create the tenant - final DefaultTenantDao tenantDao = new DefaultTenantDao(dbi, clock, cacheControllerDispatcher, new DefaultNonEntityDao(dbi)); + final DefaultTenantDao tenantDao = new DefaultTenantDao(dbi, clock, cacheControllerDispatcher, new DefaultNonEntityDao(dbi), securityConfig); tenant = new DefaultTenant(UUID.randomUUID(), null, null, UUID.randomUUID().toString(), UUID.randomUUID().toString(), UUID.randomUUID().toString()); tenantDao.create(new TenantModelDao(tenant), internalCallContext); @@ -60,7 +60,7 @@ public void beforeMethod() throws Exception { dbConfig.setUsername(helper.getUsername()); dbConfig.setPassword(helper.getPassword()); - final KillbillJdbcTenantRealm jdbcRealm = new KillbillJdbcTenantRealm(shiroDataSource); + final KillbillJdbcTenantRealm jdbcRealm = new KillbillJdbcTenantRealm(shiroDataSource, securityConfig); jdbcRealm.setDataSource(new HikariDataSource(dbConfig)); securityManager = new DefaultSecurityManager(jdbcRealm); diff --git a/profiles/killbill/src/test/resources/killbill.properties b/profiles/killbill/src/test/resources/killbill.properties index 470500ce08..08ec536775 100644 --- a/profiles/killbill/src/test/resources/killbill.properties +++ b/profiles/killbill/src/test/resources/killbill.properties @@ -27,4 +27,4 @@ org.killbill.payment.retry.days=8,8,8 org.killbill.osgi.bundle.install.dir=/var/tmp/somethingthatdoesnotexist # Speed up from the (more secure) default -org.killbill.server.multitenant.hash_iterations=10 +org.killbill.security.shiroNbHashIterations=10 diff --git a/profiles/killpay/pom.xml b/profiles/killpay/pom.xml index 4544cc3ae2..8377c3f312 100644 --- a/profiles/killpay/pom.xml +++ b/profiles/killpay/pom.xml @@ -20,7 +20,7 @@ killbill-profiles org.kill-bill.billing - 0.15.3-SNAPSHOT + 0.15.7-SNAPSHOT ../pom.xml killbill-profiles-killpay diff --git a/profiles/pom.xml b/profiles/pom.xml index 6f18124b66..71410e80d6 100644 --- a/profiles/pom.xml +++ b/profiles/pom.xml @@ -19,7 +19,7 @@ killbill org.kill-bill.billing - 0.15.3-SNAPSHOT + 0.15.7-SNAPSHOT ../pom.xml killbill-profiles diff --git a/subscription/pom.xml b/subscription/pom.xml index efcb4d48b0..7eca1bf797 100644 --- a/subscription/pom.xml +++ b/subscription/pom.xml @@ -19,7 +19,7 @@ killbill org.kill-bill.billing - 0.15.3-SNAPSHOT + 0.15.7-SNAPSHOT ../pom.xml killbill-subscription diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java index 39fdf2bd1e..01746a9618 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java @@ -206,7 +206,6 @@ private List toEvents(final DefaultSubscriptionBase defau .setEventPriceList(cur.getPriceList()) .setActiveVersion(defaultSubscriptionBase.getActiveVersion()) .setEffectiveDate(cur.getEventTime()) - .setProcessedDate(now) .setRequestedDate(now) .setFromDisk(true); @@ -256,11 +255,11 @@ private List toEvents(final DefaultSubscriptionBase defau int compForApiType(final SubscriptionBaseEvent o1, final SubscriptionBaseEvent o2, final ApiEventType type) { ApiEventType apiO1 = null; if (o1.getType() == EventType.API_USER) { - apiO1 = ((ApiEvent) o1).getEventType(); + apiO1 = ((ApiEvent) o1).getApiEventType(); } ApiEventType apiO2 = null; if (o2.getType() == EventType.API_USER) { - apiO2 = ((ApiEvent) o2).getEventType(); + apiO2 = ((ApiEvent) o2).getApiEventType(); } if (apiO1 != null && apiO1.equals(type)) { return -1; diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java index 5ec872351c..33a57d3dcb 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -119,7 +118,6 @@ public DefaultSubscriptionInternalApi(final SubscriptionDao dao, this.notificationQueueService = notificationQueueService; } - @Override public SubscriptionBase createSubscription(final UUID bundleId, final PlanPhaseSpecifier spec, final List overrides, final DateTime requestedDateWithMs, final InternalCallContext context) throws SubscriptionBaseApiException { try { @@ -509,12 +507,11 @@ public DateTime apply(final NotificationEventWithMetadata inp return input.getEffectiveDate(); } }); - } catch(NoSuchNotificationQueue noSuchNotificationQueue) { + } catch (NoSuchNotificationQueue noSuchNotificationQueue) { throw new IllegalStateException(noSuchNotificationQueue); } } - @Override public Map getNextFutureEventForSubscriptions(final SubscriptionBaseTransitionType eventType, final InternalCallContext internalCallContext) { final Iterable events = dao.getFutureEventsForAccount(internalCallContext); @@ -524,7 +521,7 @@ public boolean apply(final SubscriptionBaseEvent input) { return (eventType == SubscriptionBaseTransitionType.PHASE && input.getType() == EventType.PHASE) || input.getType() != EventType.PHASE; } }); - final Map result = filteredEvents.iterator().hasNext() ? new HashMap() : ImmutableMap.of(); + final Map result = filteredEvents.iterator().hasNext() ? new HashMap() : ImmutableMap.of(); for (SubscriptionBaseEvent cur : filteredEvents) { final DateTime targetDate = result.get(cur.getSubscriptionId()); if (targetDate == null || targetDate.compareTo(cur.getEffectiveDate()) > 0) { diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultDeletedEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultDeletedEvent.java deleted file mode 100644 index 506cd7a0e5..0000000000 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultDeletedEvent.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.subscription.api.timeline; - -import java.util.UUID; - -import org.joda.time.DateTime; - -import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.DeletedEvent; - -public class DefaultDeletedEvent implements DeletedEvent { - - private final UUID id; - private final DateTime effectiveDate; - - public DefaultDeletedEvent(final UUID id, final DateTime effectiveDate) { - this.id = id; - this.effectiveDate = effectiveDate; - } - - @Override - public UUID getEventId() { - return id; - } - - public DateTime getEffectiveDate() { - return effectiveDate; - } -} diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultNewEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultNewEvent.java deleted file mode 100644 index 79303a6c1d..0000000000 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultNewEvent.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.subscription.api.timeline; - -import java.util.UUID; - -import org.joda.time.DateTime; - -import org.killbill.billing.catalog.api.PlanPhaseSpecifier; -import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; -import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.NewEvent; - -public class DefaultNewEvent implements NewEvent { - - private final UUID subscriptionId; - private final PlanPhaseSpecifier spec; - private final DateTime requestedDate; - private final SubscriptionBaseTransitionType transitionType; - - public DefaultNewEvent(final UUID subscriptionId, final PlanPhaseSpecifier spec, final DateTime requestedDate, final SubscriptionBaseTransitionType transitionType) { - this.subscriptionId = subscriptionId; - this.spec = spec; - this.requestedDate = requestedDate; - this.transitionType = transitionType; - } - - @Override - public PlanPhaseSpecifier getPlanPhaseSpecifier() { - return spec; - } - - @Override - public DateTime getRequestedDate() { - return requestedDate; - } - - @Override - public SubscriptionBaseTransitionType getSubscriptionTransitionType() { - return transitionType; - } - - public UUID getSubscriptionId() { - return subscriptionId; - } -} diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultRepairSubscriptionEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultRepairSubscriptionEvent.java deleted file mode 100644 index 54391750e0..0000000000 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultRepairSubscriptionEvent.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.subscription.api.timeline; - -import java.util.UUID; - -import org.joda.time.DateTime; - -import org.killbill.billing.events.BusEventBase; -import org.killbill.billing.events.RepairSubscriptionInternalEvent; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class DefaultRepairSubscriptionEvent extends BusEventBase implements RepairSubscriptionInternalEvent { - - private final UUID bundleId; - private final UUID accountId; - private final DateTime effectiveDate; - - - @JsonCreator - public DefaultRepairSubscriptionEvent(@JsonProperty("accountId") final UUID accountId, - @JsonProperty("bundleId") final UUID bundleId, - @JsonProperty("effectiveDate") final DateTime effectiveDate, - @JsonProperty("searchKey1") final Long searchKey1, - @JsonProperty("searchKey2") final Long searchKey2, - @JsonProperty("userToken") final UUID userToken) { - super(searchKey1, searchKey2, userToken); - this.bundleId = bundleId; - this.accountId = accountId; - this.effectiveDate = effectiveDate; - } - - @JsonIgnore - @Override - public BusInternalEventType getBusEventType() { - return BusInternalEventType.BUNDLE_REPAIR; - } - - @Override - public UUID getBundleId() { - return bundleId; - } - - @Override - public UUID getAccountId() { - return accountId; - } - - @Override - public DateTime getEffectiveDate() { - return effectiveDate; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result - + ((accountId == null) ? 0 : accountId.hashCode()); - result = prime * result - + ((bundleId == null) ? 0 : bundleId.hashCode()); - result = prime * result - + ((effectiveDate == null) ? 0 : effectiveDate.hashCode()); - return result; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final DefaultRepairSubscriptionEvent other = (DefaultRepairSubscriptionEvent) obj; - if (accountId == null) { - if (other.accountId != null) { - return false; - } - } else if (!accountId.equals(other.accountId)) { - return false; - } - if (bundleId == null) { - if (other.bundleId != null) { - return false; - } - } else if (!bundleId.equals(other.bundleId)) { - return false; - } - if (effectiveDate == null) { - if (other.effectiveDate != null) { - return false; - } - } else if (effectiveDate.compareTo(other.effectiveDate) != 0) { - return false; - } - return true; - } - -} diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java index e81a70367e..73de18abbe 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java @@ -16,7 +16,6 @@ package org.killbill.billing.subscription.api.timeline; -import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; @@ -26,7 +25,6 @@ import javax.annotation.Nullable; import org.joda.time.DateTime; - import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.catalog.api.Catalog; import org.killbill.billing.catalog.api.CatalogApiException; @@ -36,48 +34,22 @@ import org.killbill.billing.catalog.api.PlanPhaseSpecifier; import org.killbill.billing.catalog.api.ProductCategory; import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; +import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase; import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData; import org.killbill.billing.subscription.events.SubscriptionBaseEvent; import org.killbill.billing.subscription.events.phase.PhaseEvent; import org.killbill.billing.subscription.events.user.ApiEvent; import org.killbill.billing.subscription.events.user.ApiEventType; - public class DefaultSubscriptionBaseTimeline implements SubscriptionBaseTimeline { private final UUID id; private final List existingEvents; - private final List newEvents; - private final List deletedEvents; private final long activeVersion; - public DefaultSubscriptionBaseTimeline(final UUID id, final long activeVersion) { - this.id = id; - this.activeVersion = activeVersion; - this.existingEvents = Collections.emptyList(); - this.deletedEvents = Collections.emptyList(); - this.newEvents = Collections.emptyList(); - } - - public DefaultSubscriptionBaseTimeline(final SubscriptionBaseTimeline input) { - this.id = input.getId(); - this.activeVersion = input.getActiveVersion(); - this.existingEvents = (input.getExistingEvents() != null) ? new ArrayList(input.getExistingEvents()) : - Collections.emptyList(); - sortExistingEvent(this.existingEvents); - this.deletedEvents = (input.getDeletedEvents() != null) ? new ArrayList(input.getDeletedEvents()) : - Collections.emptyList(); - this.newEvents = (input.getNewEvents() != null) ? new ArrayList(input.getNewEvents()) : - Collections.emptyList(); - sortNewEvent(this.newEvents); - } - - // CTOR for returning events only - public DefaultSubscriptionBaseTimeline(final SubscriptionDataRepair input, final Catalog catalog) throws CatalogApiException { + public DefaultSubscriptionBaseTimeline(final DefaultSubscriptionBase input, final Catalog catalog) throws CatalogApiException { this.id = input.getId(); this.existingEvents = toExistingEvents(catalog, input.getActiveVersion(), input.getCategory(), input.getEvents()); - this.deletedEvents = null; - this.newEvents = null; this.activeVersion = input.getActiveVersion(); } @@ -135,7 +107,7 @@ private List toExistingEvents(final Catalog catalog, final long a case API_USER: final ApiEvent userEV = (ApiEvent) cur; - apiType = userEV.getEventType(); + apiType = userEV.getApiEventType(); planName = userEV.getEventPlan(); planPhaseName = userEV.getEventPlanPhase(); final Plan plan = (userEV.getEventPlan() != null) ? catalog.findPlan(userEV.getEventPlan(), cur.getRequestedDate(), startDate) : null; @@ -199,89 +171,6 @@ public String getPlanPhaseName() { return result; } - - /* - - private List toExistingEvents(final Catalog catalog, final long processingVersion, final ProductCategory category, final List events, List result) - throws CatalogApiException { - - - String prevProductName = null; - BillingPeriod prevBillingPeriod = null; - String prevPriceListName = null; - PhaseType prevPhaseType = null; - - DateTime startDate = null; - - for (final SubscriptionBaseEvent cur : events) { - - if (processingVersion != cur.getActiveVersion()) { - continue; - } - - // First active event is used to figure out which catalog version to use. - startDate = (startDate == null && cur.getActiveVersion() == processingVersion) ? cur.getEffectiveDate() : startDate; - - String productName = null; - BillingPeriod billingPeriod = null; - String priceListName = null; - PhaseType phaseType = null; - - ApiEventType apiType = null; - switch (cur.getType()) { - case PHASE: - PhaseEvent phaseEV = (PhaseEvent) cur; - phaseType = catalog.findPhase(phaseEV.getPhase(), cur.getEffectiveDate(), startDate).getPhaseType(); - productName = prevProductName; - billingPeriod = prevBillingPeriod; - priceListName = prevPriceListName; - break; - - case API_USER: - ApiEvent userEV = (ApiEvent) cur; - apiType = userEV.getEventType(); - Plan plan = (userEV.getEventPlan() != null) ? catalog.findPlan(userEV.getEventPlan(), cur.getRequestedDate(), startDate) : null; - phaseType = (userEV.getEventPlanPhase() != null) ? catalog.findPhase(userEV.getEventPlanPhase(), cur.getEffectiveDate(), startDate).getPhaseType() : prevPhaseType; - productName = (plan != null) ? plan.getProduct().getName() : prevProductName; - billingPeriod = (plan != null) ? plan.getBillingPeriod() : prevBillingPeriod; - priceListName = (userEV.getPriceList() != null) ? userEV.getPriceList() : prevPriceListName; - break; - } - - final SubscriptionBaseTransitionType transitionType = SubscriptionBaseTransitionData.toSubscriptionTransitionType(cur.getType(), apiType); - - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(productName, category, billingPeriod, priceListName, phaseType); - result.add(new ExistingEvent() { - @Override - public SubscriptionBaseTransitionType getSubscriptionTransitionType() { - return transitionType; - } - @Override - public DateTime getRequestedDate() { - return cur.getRequestedDate(); - } - @Override - public PlanPhaseSpecifier getPlanPhaseSpecifier() { - return spec; - } - @Override - public UUID getEventId() { - return cur.getId(); - } - @Override - public DateTime getEffectiveDate() { - return cur.getEffectiveDate(); - } - }); - prevProductName = productName; - prevBillingPeriod = billingPeriod; - prevPriceListName = priceListName; - prevPhaseType = phaseType; - } - } - */ - - @Override public UUID getId() { return id; @@ -297,16 +186,6 @@ public DateTime getUpdatedDate() { throw new UnsupportedOperationException(); } - @Override - public List getDeletedEvents() { - return deletedEvents; - } - - @Override - public List getNewEvents() { - return newEvents; - } - @Override public List getExistingEvents() { return existingEvents; @@ -317,7 +196,6 @@ public long getActiveVersion() { return activeVersion; } - private void sortExistingEvent(final List events) { if (events != null) { Collections.sort(events, new Comparator() { @@ -329,14 +207,4 @@ public int compare(final ExistingEvent arg0, final ExistingEvent arg1) { } } - private void sortNewEvent(final List events) { - if (events != null) { - Collections.sort(events, new Comparator() { - @Override - public int compare(final NewEvent arg0, final NewEvent arg1) { - return arg0.getRequestedDate().compareTo(arg1.getRequestedDate()); - } - }); - } - } } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java index 4171cc09e5..4dca0a1ff5 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java @@ -16,113 +16,59 @@ package org.killbill.billing.subscription.api.timeline; -import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.UUID; -import javax.annotation.Nullable; - import org.joda.time.DateTime; - import org.killbill.billing.ErrorCode; -import org.killbill.billing.catalog.api.Catalog; +import org.killbill.billing.callcontext.InternalTenantContext; import org.killbill.billing.catalog.api.CatalogApiException; import org.killbill.billing.catalog.api.CatalogService; -import org.killbill.billing.catalog.api.ProductCategory; import org.killbill.billing.subscription.api.SubscriptionApiBase; +import org.killbill.billing.subscription.api.SubscriptionBase; import org.killbill.billing.subscription.api.SubscriptionBaseApiService; -import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; +import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase; import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle; import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle; -import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition; -import org.killbill.billing.subscription.api.user.SubscriptionBuilder; -import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase; -import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData; -import org.killbill.billing.subscription.engine.addon.AddonUtils; import org.killbill.billing.subscription.engine.dao.SubscriptionDao; import org.killbill.billing.subscription.events.SubscriptionBaseEvent; -import org.killbill.billing.subscription.glue.DefaultSubscriptionModule; -import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.NewEvent; -import org.killbill.billing.subscription.api.SubscriptionBase; -import org.killbill.billing.util.callcontext.CallContext; import org.killbill.billing.util.callcontext.InternalCallContextFactory; -import org.killbill.billing.callcontext.InternalTenantContext; import org.killbill.billing.util.callcontext.TenantContext; import org.killbill.clock.Clock; -import com.google.common.base.Function; -import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.inject.Inject; -import com.google.inject.name.Named; public class DefaultSubscriptionBaseTimelineApi extends SubscriptionApiBase implements SubscriptionBaseTimelineApi { - private final RepairSubscriptionLifecycleDao repairDao; private final CatalogService catalogService; private final InternalCallContextFactory internalCallContextFactory; - private final AddonUtils addonUtils; - - private final SubscriptionBaseApiService repairApiService; - - private enum RepairType { - BASE_REPAIR, - ADD_ON_REPAIR, - STANDALONE_REPAIR - } @Inject public DefaultSubscriptionBaseTimelineApi(final CatalogService catalogService, final SubscriptionBaseApiService apiService, - @Named(DefaultSubscriptionModule.REPAIR_NAMED) final RepairSubscriptionLifecycleDao repairDao, final SubscriptionDao dao, - @Named(DefaultSubscriptionModule.REPAIR_NAMED) final SubscriptionBaseApiService repairApiService, - final InternalCallContextFactory internalCallContextFactory, final Clock clock, final AddonUtils addonUtils) { + final SubscriptionDao dao, + final InternalCallContextFactory internalCallContextFactory, + final Clock clock) { super(dao, apiService, clock, catalogService); this.catalogService = catalogService; - this.repairDao = repairDao; this.internalCallContextFactory = internalCallContextFactory; - this.repairApiService = repairApiService; - this.addonUtils = addonUtils; } @Override public BundleBaseTimeline getBundleTimeline(final SubscriptionBaseBundle bundle, final TenantContext context) throws SubscriptionBaseRepairException { - return getBundleTimelineInternal(bundle, bundle.getExternalKey(), context); - } - - @Override - public BundleBaseTimeline getBundleTimeline(final UUID accountId, final String bundleName, final TenantContext context) - throws SubscriptionBaseRepairException { - final List bundles = dao.getSubscriptionBundlesForAccountAndKey(accountId, bundleName, internalCallContextFactory.createInternalTenantContext(context)); - final SubscriptionBaseBundle bundle = bundles.size() > 0 ? bundles.get(bundles.size() - 1) : null; - return getBundleTimelineInternal(bundle, bundleName + " [accountId= " + accountId.toString() + "]", context); - } - - @Override - public BundleBaseTimeline getBundleTimeline(final UUID bundleId, final TenantContext context) throws SubscriptionBaseRepairException { - - final SubscriptionBaseBundle bundle = dao.getSubscriptionBundleFromId(bundleId, internalCallContextFactory.createInternalTenantContext(context)); - return getBundleTimelineInternal(bundle, bundleId.toString(), context); - } - - private BundleBaseTimeline getBundleTimelineInternal(final SubscriptionBaseBundle bundle, final String descBundle, final TenantContext context) throws SubscriptionBaseRepairException { try { - if (bundle == null) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_UNKNOWN_BUNDLE, descBundle); - } final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(bundle.getAccountId(), context); - final List subscriptions = convertToSubscriptionsDataRepair(dao.getSubscriptions(bundle.getId(), - ImmutableList.of(), - internalTenantContext)); + final List subscriptions = dao.getSubscriptions(bundle.getId(), + ImmutableList.of(), + internalTenantContext); if (subscriptions.size() == 0) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_NO_ACTIVE_SUBSCRIPTIONS, bundle.getId()); + throw new SubscriptionBaseRepairException(ErrorCode.SUB_NO_ACTIVE_SUBSCRIPTIONS, bundle.getId()); } final String viewId = getViewId(((DefaultSubscriptionBaseBundle) bundle).getLastSysUpdateDate(), subscriptions); final List repairs = createGetSubscriptionRepairList(subscriptions, Collections.emptyList(), internalTenantContext); @@ -132,273 +78,7 @@ private BundleBaseTimeline getBundleTimelineInternal(final SubscriptionBaseBundl } } - private List convertToSubscriptionsDataRepair(List input) { - return new ArrayList(Collections2.transform(input, new Function() { - @Override - public SubscriptionDataRepair apply(@Nullable final SubscriptionBase subscription) { - return convertToSubscriptionDataRepair((DefaultSubscriptionBase) subscription); - } - })); - } - private SubscriptionDataRepair convertToSubscriptionDataRepair(DefaultSubscriptionBase input) { - return new SubscriptionDataRepair(input, repairApiService, (SubscriptionDao) repairDao, clock, addonUtils, catalogService, internalCallContextFactory); - } - - @Override - public BundleBaseTimeline repairBundle(final BundleBaseTimeline input, final boolean dryRun, final CallContext context) throws SubscriptionBaseRepairException { - final InternalTenantContext tenantContext = internalCallContextFactory.createInternalTenantContext(context); - try { - final SubscriptionBaseBundle bundle = dao.getSubscriptionBundleFromId(input.getId(), tenantContext); - if (bundle == null) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_UNKNOWN_BUNDLE, input.getId()); - } - - // Subscriptions are ordered with BASE subscription first-- if exists - final List subscriptions = convertToSubscriptionsDataRepair(dao.getSubscriptions(input.getId(), ImmutableList.of(), tenantContext)); - if (subscriptions.size() == 0) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_NO_ACTIVE_SUBSCRIPTIONS, input.getId()); - } - - final String viewId = getViewId(((DefaultSubscriptionBaseBundle) bundle).getLastSysUpdateDate(), subscriptions); - if (!viewId.equals(input.getViewId())) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_VIEW_CHANGED, input.getId(), input.getViewId(), viewId); - } - - DateTime firstDeletedBPEventTime = null; - DateTime lastRemainingBPEventTime = null; - - boolean isBasePlanRecreate = false; - DateTime newBundleStartDate = null; - - SubscriptionDataRepair baseSubscriptionRepair = null; - final List addOnSubscriptionInRepair = new LinkedList(); - final List inRepair = new LinkedList(); - for (final SubscriptionBase cur : subscriptions) { - final SubscriptionBaseTimeline curRepair = findAndCreateSubscriptionRepair(cur.getId(), input.getSubscriptions()); - if (curRepair != null) { - final SubscriptionDataRepair curInputRepair = ((SubscriptionDataRepair) cur); - final List remaining = getRemainingEventsAndValidateDeletedEvents(curInputRepair, firstDeletedBPEventTime, curRepair.getDeletedEvents()); - - final boolean isPlanRecreate = (curRepair.getNewEvents().size() > 0 - && (curRepair.getNewEvents().get(0).getSubscriptionTransitionType() == SubscriptionBaseTransitionType.CREATE - || curRepair.getNewEvents().get(0).getSubscriptionTransitionType() == SubscriptionBaseTransitionType.RE_CREATE)); - - final DateTime newSubscriptionStartDate = isPlanRecreate ? curRepair.getNewEvents().get(0).getRequestedDate() : null; - - if (isPlanRecreate && remaining.size() != 0) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_SUB_RECREATE_NOT_EMPTY, cur.getId(), cur.getBundleId()); - } - - if (!isPlanRecreate && remaining.size() == 0) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_SUB_EMPTY, cur.getId(), cur.getBundleId()); - } - - if (cur.getCategory() == ProductCategory.BASE) { - - final int bpTransitionSize = ((DefaultSubscriptionBase) cur).getAllTransitions().size(); - lastRemainingBPEventTime = (remaining.size() > 0) ? curInputRepair.getAllTransitions().get(remaining.size() - 1).getEffectiveTransitionTime() : null; - firstDeletedBPEventTime = (remaining.size() < bpTransitionSize) ? curInputRepair.getAllTransitions().get(remaining.size()).getEffectiveTransitionTime() : null; - - isBasePlanRecreate = isPlanRecreate; - newBundleStartDate = newSubscriptionStartDate; - } - - if (curRepair.getNewEvents().size() > 0) { - final DateTime lastRemainingEventTime = (remaining.size() == 0) ? null : curInputRepair.getAllTransitions().get(remaining.size() - 1).getEffectiveTransitionTime(); - validateFirstNewEvent(curInputRepair, curRepair.getNewEvents().get(0), lastRemainingBPEventTime, lastRemainingEventTime); - } - - final SubscriptionDataRepair curOutputRepair = createSubscriptionDataRepair(curInputRepair, newBundleStartDate, newSubscriptionStartDate, remaining, tenantContext); - repairDao.initializeRepair(curInputRepair.getId(), remaining, tenantContext); - inRepair.add(curOutputRepair); - if (curOutputRepair.getCategory() == ProductCategory.ADD_ON) { - // Check if ADD_ON RE_CREATE is before BP start - if (isPlanRecreate && (subscriptions.get(0)).getStartDate().isAfter(curRepair.getNewEvents().get(0).getRequestedDate())) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_AO_CREATE_BEFORE_BP_START, cur.getId(), cur.getBundleId()); - } - addOnSubscriptionInRepair.add(curOutputRepair); - } else if (curOutputRepair.getCategory() == ProductCategory.BASE) { - baseSubscriptionRepair = curOutputRepair; - } - } - } - - final RepairType repairType = getRepairType(subscriptions.get(0), (baseSubscriptionRepair != null)); - switch (repairType) { - case BASE_REPAIR: - // We need to add any existing addon that are not in the input repair list - for (final SubscriptionBase cur : subscriptions) { - if (cur.getCategory() == ProductCategory.ADD_ON && !inRepair.contains(cur)) { - final SubscriptionDataRepair curOutputRepair = createSubscriptionDataRepair((SubscriptionDataRepair) cur, newBundleStartDate, null, - ((SubscriptionDataRepair) cur).getEvents(), tenantContext); - repairDao.initializeRepair(curOutputRepair.getId(), ((SubscriptionDataRepair) cur).getEvents(), tenantContext); - inRepair.add(curOutputRepair); - addOnSubscriptionInRepair.add(curOutputRepair); - } - } - break; - case ADD_ON_REPAIR: - // We need to set the baseSubscription as it is useful to calculate addon validity - final SubscriptionDataRepair baseSubscription = (SubscriptionDataRepair) subscriptions.get(0); - baseSubscriptionRepair = createSubscriptionDataRepair(baseSubscription, baseSubscription.getBundleStartDate(), baseSubscription.getAlignStartDate(), - baseSubscription.getEvents(), tenantContext); - break; - case STANDALONE_REPAIR: - default: - break; - } - - validateBasePlanRecreate(isBasePlanRecreate, subscriptions, input.getSubscriptions()); - validateInputSubscriptionsKnown(subscriptions, input.getSubscriptions()); - - final Collection newEvents = createOrderedNewEventInput(input.getSubscriptions()); - for (final NewEvent newEvent : newEvents) { - final DefaultNewEvent cur = (DefaultNewEvent) newEvent; - final SubscriptionDataRepair curDataRepair = findSubscriptionDataRepair(cur.getSubscriptionId(), inRepair); - if (curDataRepair == null) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_UNKNOWN_SUBSCRIPTION, cur.getSubscriptionId()); - } - curDataRepair.addNewRepairEvent(cur, baseSubscriptionRepair, addOnSubscriptionInRepair, context); - } - - if (dryRun) { - baseSubscriptionRepair.addFutureAddonCancellation(addOnSubscriptionInRepair, context); - - final List repairs = createGetSubscriptionRepairList(subscriptions, convertDataRepair(inRepair, tenantContext), tenantContext); - return createGetBundleRepair(input.getId(), bundle.getExternalKey(), input.getViewId(), repairs); - } else { - dao.repair(bundle.getAccountId(), input.getId(), inRepair, internalCallContextFactory.createInternalCallContext(bundle.getAccountId(), context)); - return getBundleTimeline(input.getId(), context); - } - } catch (CatalogApiException e) { - throw new SubscriptionBaseRepairException(e); - } finally { - repairDao.cleanup(tenantContext); - } - } - - private RepairType getRepairType(final SubscriptionBase firstSubscription, final boolean gotBaseSubscription) { - if (firstSubscription.getCategory() == ProductCategory.BASE) { - return gotBaseSubscription ? RepairType.BASE_REPAIR : RepairType.ADD_ON_REPAIR; - } else { - return RepairType.STANDALONE_REPAIR; - } - } - - private void validateBasePlanRecreate(final boolean isBasePlanRecreate, final List subscriptions, final List input) - throws SubscriptionBaseRepairException { - if (!isBasePlanRecreate) { - return; - } - if (subscriptions.size() != input.size()) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_BP_RECREATE_MISSING_AO, subscriptions.get(0).getBundleId()); - } - for (final SubscriptionBaseTimeline cur : input) { - if (cur.getNewEvents().size() != 0 - && (cur.getNewEvents().get(0).getSubscriptionTransitionType() != SubscriptionBaseTransitionType.CREATE - && cur.getNewEvents().get(0).getSubscriptionTransitionType() != SubscriptionBaseTransitionType.RE_CREATE)) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_BP_RECREATE_MISSING_AO_CREATE, subscriptions.get(0).getBundleId()); - } - } - } - - private void validateInputSubscriptionsKnown(final List subscriptions, final List input) - throws SubscriptionBaseRepairException { - for (final SubscriptionBaseTimeline cur : input) { - boolean found = false; - for (final SubscriptionBase s : subscriptions) { - if (s.getId().equals(cur.getId())) { - found = true; - break; - } - } - if (!found) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_UNKNOWN_SUBSCRIPTION, cur.getId()); - } - } - } - - private void validateFirstNewEvent(final DefaultSubscriptionBase data, final NewEvent firstNewEvent, final DateTime lastBPRemainingTime, final DateTime lastRemainingTime) - throws SubscriptionBaseRepairException { - if (lastBPRemainingTime != null && - firstNewEvent.getRequestedDate().isBefore(lastBPRemainingTime)) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_NEW_EVENT_BEFORE_LAST_BP_REMAINING, firstNewEvent.getSubscriptionTransitionType(), data.getId()); - } - if (lastRemainingTime != null && - firstNewEvent.getRequestedDate().isBefore(lastRemainingTime)) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_NEW_EVENT_BEFORE_LAST_AO_REMAINING, firstNewEvent.getSubscriptionTransitionType(), data.getId()); - } - - } - - private Collection createOrderedNewEventInput(final List subscriptionsReapir) { - final TreeSet newEventSet = new TreeSet(new Comparator() { - @Override - public int compare(final NewEvent o1, final NewEvent o2) { - return o1.getRequestedDate().compareTo(o2.getRequestedDate()); - } - }); - for (final SubscriptionBaseTimeline cur : subscriptionsReapir) { - for (final NewEvent e : cur.getNewEvents()) { - newEventSet.add(new DefaultNewEvent(cur.getId(), e.getPlanPhaseSpecifier(), e.getRequestedDate(), e.getSubscriptionTransitionType())); - } - } - - return newEventSet; - } - - private List getRemainingEventsAndValidateDeletedEvents(final SubscriptionDataRepair data, final DateTime firstBPDeletedTime, - final List deletedEvents) - throws SubscriptionBaseRepairException { - if (deletedEvents == null || deletedEvents.size() == 0) { - return data.getEvents(); - } - - int nbDeleted = 0; - final LinkedList result = new LinkedList(); - for (final SubscriptionBaseEvent cur : data.getEvents()) { - - boolean foundDeletedEvent = false; - for (final SubscriptionBaseTimeline.DeletedEvent d : deletedEvents) { - if (cur.getId().equals(d.getEventId())) { - foundDeletedEvent = true; - nbDeleted++; - break; - } - } - if (!foundDeletedEvent && nbDeleted > 0) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_INVALID_DELETE_SET, cur.getId(), data.getId()); - } - if (firstBPDeletedTime != null && - !cur.getEffectiveDate().isBefore(firstBPDeletedTime) && - !foundDeletedEvent) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_MISSING_AO_DELETE_EVENT, cur.getId(), data.getId()); - } - - if (nbDeleted == 0) { - result.add(cur); - } - } - - if (nbDeleted != deletedEvents.size()) { - for (final SubscriptionBaseTimeline.DeletedEvent d : deletedEvents) { - boolean found = false; - for (final SubscriptionBaseTransition cur : data.getAllTransitions()) { - if (((SubscriptionBaseTransitionData) cur).getId().equals(d.getEventId())) { - found = true; - } - } - if (!found) { - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_NON_EXISTENT_DELETE_EVENT, d.getEventId(), data.getId()); - } - } - - } - - return result; - } - - private String getViewId(final DateTime lastUpdateBundleDate, final List subscriptions) { + private String getViewId(final DateTime lastUpdateBundleDate, final List subscriptions) { final StringBuilder tmp = new StringBuilder(); long lastOrderedId = -1; for (final SubscriptionBase cur : subscriptions) { @@ -445,7 +125,7 @@ public String getExternalKey() { }; } - private List createGetSubscriptionRepairList(final List subscriptions, final List inRepair, final InternalTenantContext tenantContext) throws CatalogApiException { + private List createGetSubscriptionRepairList(final List subscriptions, final List inRepair, final InternalTenantContext tenantContext) throws CatalogApiException { final List result = new LinkedList(); final Set repairIds = new TreeSet(); @@ -456,62 +136,11 @@ private List createGetSubscriptionRepairList(final Lis for (final SubscriptionBase cur : subscriptions) { if (!repairIds.contains(cur.getId())) { - result.add(new DefaultSubscriptionBaseTimeline((SubscriptionDataRepair) cur, catalogService.getFullCatalog(tenantContext))); + result.add(new DefaultSubscriptionBaseTimeline((DefaultSubscriptionBase) cur, catalogService.getFullCatalog(tenantContext))); } } - - return result; - } - - private List convertDataRepair(final List input, final InternalTenantContext tenantContext) throws CatalogApiException { - final List result = new LinkedList(); - for (final SubscriptionDataRepair cur : input) { - result.add(new DefaultSubscriptionBaseTimeline(cur, catalogService.getFullCatalog(tenantContext))); - } - return result; } - private SubscriptionDataRepair findSubscriptionDataRepair(final UUID targetId, final List input) { - for (final SubscriptionDataRepair cur : input) { - if (cur.getId().equals(targetId)) { - return cur; - } - } - - return null; - } - - private SubscriptionDataRepair createSubscriptionDataRepair(final DefaultSubscriptionBase curData, final DateTime newBundleStartDate, final DateTime newSubscriptionStartDate, - final List initialEvents, final InternalTenantContext tenantContext) throws CatalogApiException { - final SubscriptionBuilder builder = new SubscriptionBuilder(curData); - builder.setActiveVersion(curData.getActiveVersion() + 1); - if (newBundleStartDate != null) { - builder.setBundleStartDate(newBundleStartDate); - } - if (newSubscriptionStartDate != null) { - builder.setAlignStartDate(newSubscriptionStartDate); - } - if (initialEvents.size() > 0) { - for (final SubscriptionBaseEvent cur : initialEvents) { - cur.setActiveVersion(builder.getActiveVersion()); - } - } - - final SubscriptionDataRepair subscriptiondataRepair = new SubscriptionDataRepair(builder, curData.getEvents(), repairApiService, (SubscriptionDao) repairDao, clock, addonUtils, catalogService, internalCallContextFactory); - final Catalog fullCatalog = catalogService.getFullCatalog(tenantContext); - subscriptiondataRepair.rebuildTransitions(curData.getEvents(), fullCatalog); - return subscriptiondataRepair; - } - - private SubscriptionBaseTimeline findAndCreateSubscriptionRepair(final UUID target, final List input) { - for (final SubscriptionBaseTimeline cur : input) { - if (target.equals(cur.getId())) { - return new DefaultSubscriptionBaseTimeline(cur); - } - } - - return null; - } } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionApiService.java deleted file mode 100644 index 6a0efd432a..0000000000 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionApiService.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.subscription.api.timeline; - -import java.util.UUID; - -import org.joda.time.DateTime; -import org.killbill.billing.catalog.api.CatalogService; -import org.killbill.billing.catalog.api.Product; -import org.killbill.billing.subscription.alignment.PlanAligner; -import org.killbill.billing.subscription.api.SubscriptionBaseApiService; -import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase; -import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseApiService; -import org.killbill.billing.subscription.engine.addon.AddonUtils; -import org.killbill.billing.subscription.engine.dao.SubscriptionDao; -import org.killbill.billing.subscription.glue.DefaultSubscriptionModule; -import org.killbill.billing.util.callcontext.CallContext; -import org.killbill.billing.util.callcontext.InternalCallContextFactory; -import org.killbill.clock.Clock; - -import com.google.inject.Inject; -import com.google.inject.name.Named; - -public class RepairSubscriptionApiService extends DefaultSubscriptionBaseApiService implements SubscriptionBaseApiService { - - @Inject - public RepairSubscriptionApiService(final Clock clock, - @Named(DefaultSubscriptionModule.REPAIR_NAMED) final SubscriptionDao dao, - final CatalogService catalogService, - final PlanAligner planAligner, - final AddonUtils addonUtils, - final InternalCallContextFactory internalCallContextFactory) { - super(clock, dao, catalogService, planAligner, addonUtils, internalCallContextFactory); - } - - // Nothing to do for repair as we pass all the repair events in the stream - @Override - public int cancelAddOnsIfRequired(final Product baseProduct, final UUID bundleId, final DateTime effectiveDate, final CallContext context) { - return 0; - } -} diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionLifecycleDao.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionLifecycleDao.java deleted file mode 100644 index dac507f889..0000000000 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionLifecycleDao.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.subscription.api.timeline; - -import java.util.List; -import java.util.UUID; - -import org.killbill.billing.subscription.events.SubscriptionBaseEvent; -import org.killbill.billing.callcontext.InternalTenantContext; - -public interface RepairSubscriptionLifecycleDao { - - public void initializeRepair(UUID subscriptionId, List initialEvents, InternalTenantContext context); - - public void cleanup(InternalTenantContext context); -} diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionDataRepair.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionDataRepair.java deleted file mode 100644 index 86ea5a5f16..0000000000 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionDataRepair.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.subscription.api.timeline; - -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -import javax.annotation.concurrent.Immutable; - -import org.joda.time.DateTime; - -import org.killbill.billing.ErrorCode; -import org.killbill.billing.ObjectType; -import org.killbill.billing.callcontext.InternalCallContext; -import org.killbill.billing.callcontext.InternalTenantContext; -import org.killbill.billing.catalog.api.Catalog; -import org.killbill.billing.catalog.api.CatalogApiException; -import org.killbill.billing.catalog.api.CatalogService; -import org.killbill.billing.catalog.api.Plan; -import org.killbill.billing.catalog.api.PlanPhasePriceOverride; -import org.killbill.billing.catalog.api.PlanPhaseSpecifier; -import org.killbill.billing.catalog.api.Product; -import org.killbill.billing.catalog.api.ProductCategory; -import org.killbill.billing.entitlement.api.Entitlement.EntitlementState; -import org.killbill.billing.subscription.api.SubscriptionBaseApiService; -import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; -import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException; -import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition; -import org.killbill.billing.subscription.api.user.SubscriptionBuilder; -import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase; -import org.killbill.billing.subscription.engine.addon.AddonUtils; -import org.killbill.billing.subscription.engine.dao.SubscriptionDao; -import org.killbill.billing.subscription.events.SubscriptionBaseEvent; -import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType; -import org.killbill.billing.subscription.events.user.ApiEventBuilder; -import org.killbill.billing.subscription.events.user.ApiEventCancel; -import org.killbill.billing.util.callcontext.CallContext; -import org.killbill.billing.util.callcontext.InternalCallContextFactory; -import org.killbill.clock.Clock; - -import com.google.common.base.Predicate; -import com.google.common.collect.Collections2; -import com.google.common.collect.ImmutableList; - -public class SubscriptionDataRepair extends DefaultSubscriptionBase { - - private final AddonUtils addonUtils; - private final Clock clock; - private final SubscriptionDao repairDao; - private final CatalogService catalogService; - private final List initialEvents; - private final InternalCallContextFactory internalCallContextFactory; - - - public SubscriptionDataRepair(final SubscriptionBuilder builder, final List initialEvents, final SubscriptionBaseApiService apiService, - final SubscriptionDao dao, final Clock clock, final AddonUtils addonUtils, final CatalogService catalogService, - final InternalCallContextFactory internalCallContextFactory) { - super(builder, apiService, clock); - this.repairDao = dao; - this.addonUtils = addonUtils; - this.clock = clock; - this.catalogService = catalogService; - this.initialEvents = initialEvents; - this.internalCallContextFactory = internalCallContextFactory; - } - - - - public SubscriptionDataRepair(final DefaultSubscriptionBase defaultSubscriptionBase, final SubscriptionBaseApiService apiService, - final SubscriptionDao dao, final Clock clock, final AddonUtils addonUtils, final CatalogService catalogService, - final InternalCallContextFactory internalCallContextFactory) { - super(defaultSubscriptionBase, apiService , clock); - this.repairDao = dao; - this.addonUtils = addonUtils; - this.clock = clock; - this.catalogService = catalogService; - this.initialEvents = defaultSubscriptionBase.getEvents(); - this.internalCallContextFactory = internalCallContextFactory; - } - - DateTime getLastUserEventEffectiveDate() { - DateTime res = null; - for (final SubscriptionBaseEvent cur : events) { - if (cur.getActiveVersion() != getActiveVersion()) { - break; - } - if (cur.getType() == EventType.PHASE) { - continue; - } - res = cur.getEffectiveDate(); - } - return res; - } - - public void addNewRepairEvent(final DefaultNewEvent input, final SubscriptionDataRepair baseSubscription, final List addonSubscriptions, final CallContext context) - throws SubscriptionBaseRepairException { - - try { - - final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(baseSubscription.getId(), ObjectType.SUBSCRIPTION, context); - - final PlanPhaseSpecifier spec = input.getPlanPhaseSpecifier(); - switch (input.getSubscriptionTransitionType()) { - case CREATE: - case RE_CREATE: - recreate(spec, ImmutableList.of(), input.getRequestedDate(), context); - checkAddonRights(baseSubscription, internalTenantContext); - break; - case CHANGE: - changePlanWithDate(spec.getProductName(), spec.getBillingPeriod(), spec.getPriceListName(), ImmutableList.of(), input.getRequestedDate(), context); - checkAddonRights(baseSubscription, internalTenantContext); - trickleDownBPEffectForAddon(addonSubscriptions, getLastUserEventEffectiveDate(), context); - break; - case CANCEL: - cancelWithDate(input.getRequestedDate(), context); - trickleDownBPEffectForAddon(addonSubscriptions, getLastUserEventEffectiveDate(), context); - break; - case PHASE: - break; - default: - throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_UNKNOWN_TYPE, input.getSubscriptionTransitionType(), id); - } - } catch (SubscriptionBaseApiException e) { - throw new SubscriptionBaseRepairException(e); - } catch (CatalogApiException e) { - throw new SubscriptionBaseRepairException(e); - } - } - - public void addFutureAddonCancellation(final List addOnSubscriptionInRepair, final CallContext context) throws CatalogApiException { - - if (getCategory() != ProductCategory.BASE) { - return; - } - - final SubscriptionBaseTransition pendingTransition = getPendingTransition(); - if (pendingTransition == null) { - return; - } - final Product baseProduct = (pendingTransition.getTransitionType() == SubscriptionBaseTransitionType.CANCEL) ? null : - pendingTransition.getNextPlan().getProduct(); - - addAddonCancellationIfRequired(addOnSubscriptionInRepair, baseProduct, pendingTransition.getEffectiveTransitionTime(), context); - } - - private void trickleDownBPEffectForAddon(final List addOnSubscriptionInRepair, final DateTime effectiveDate, final CallContext context) - throws SubscriptionBaseApiException, CatalogApiException { - - if (getCategory() != ProductCategory.BASE) { - return; - } - - final Product baseProduct = (getState() == EntitlementState.CANCELLED) ? - null : getCurrentPlan().getProduct(); - addAddonCancellationIfRequired(addOnSubscriptionInRepair, baseProduct, effectiveDate, context); - } - - private void addAddonCancellationIfRequired(final List addOnSubscriptionInRepair, final Product baseProduct, - final DateTime effectiveDate, final CallContext context) throws CatalogApiException { - - final DateTime now = clock.getUTCNow(); - final Iterator it = addOnSubscriptionInRepair.iterator(); - while (it.hasNext()) { - final SubscriptionDataRepair cur = it.next(); - if (cur.getState() == EntitlementState.CANCELLED || - cur.getCategory() != ProductCategory.ADD_ON) { - continue; - } - final Plan addonCurrentPlan = cur.getCurrentPlan(); - final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(cur.getId(), ObjectType.SUBSCRIPTION, context); - - if (baseProduct == null || - addonUtils.isAddonIncludedFromProdName(baseProduct.getName(), addonCurrentPlan, effectiveDate, internalCallContext) || - !addonUtils.isAddonAvailableFromProdName(baseProduct.getName(), addonCurrentPlan, effectiveDate, internalCallContext)) { - - final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder() - .setSubscriptionId(cur.getId()) - .setActiveVersion(cur.getActiveVersion()) - .setProcessedDate(now) - .setEffectiveDate(effectiveDate) - .setRequestedDate(now) - .setFromDisk(true)); - repairDao.cancelSubscription(cur, cancelEvent, internalCallContext, 0); - final Catalog fullCatalog = catalogService.getFullCatalog(internalCallContext); - cur.rebuildTransitions(repairDao.getEventsForSubscription(cur.getId(), internalCallContextFactory.createInternalTenantContext(context)), fullCatalog); - } - } - } - - private void checkAddonRights(final SubscriptionDataRepair baseSubscription, final InternalTenantContext internalTenantContext) - throws SubscriptionBaseApiException, CatalogApiException { - if (getCategory() == ProductCategory.ADD_ON) { - addonUtils.checkAddonCreationRights(baseSubscription, getCurrentPlan(), clock.getUTCNow(), internalTenantContext); - } - } - - public List getEvents() { - return events; - } - - public List getInitialEvents() { - return initialEvents; - } - - public Collection getNewEvents() { - return Collections2.filter(events, new Predicate() { - @Override - public boolean apply(final SubscriptionBaseEvent input) { - return !initialEvents.contains(input); - } - }); - } -} diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java index 8a36c06ed5..4abe2f72e7 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java @@ -102,7 +102,6 @@ private SubscriptionBaseEvent createEvent(final boolean firstEvent, final Existi .setEventPlanPhase(currentPhase.getName()) .setEventPriceList(spec.getPriceListName()) .setActiveVersion(subscription.getActiveVersion()) - .setProcessedDate(clock.getUTCNow()) .setEffectiveDate(effectiveDate) .setRequestedDate(effectiveDate) .setFromDisk(true); @@ -241,7 +240,6 @@ public SubscriptionBaseBundle transferBundle(final UUID sourceAccountId, final U final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder() .setSubscriptionId(cur.getId()) .setActiveVersion(cur.getActiveVersion()) - .setProcessedDate(clock.getUTCNow()) .setEffectiveDate(effectiveCancelDate) .setRequestedDate(effectiveTransferDate) .setFromDisk(true)); diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java index a7ae3e9a48..184c3dbadd 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java @@ -414,7 +414,7 @@ public SubscriptionBaseTransitionData getTransitionFromEvent(final SubscriptionB } // Since UNCANCEL are not part of the transitions, we compute a new 'UNCANCEL' transition based on the event right before that UNCANCEL // This is used to be able to send a bus event for uncancellation - if (prev != null && event.getType() == EventType.API_USER && ((ApiEvent) event).getEventType() == ApiEventType.UNCANCEL) { + if (prev != null && event.getType() == EventType.API_USER && ((ApiEvent) event).getApiEventType() == ApiEventType.UNCANCEL) { final SubscriptionBaseTransitionData withSeq = new SubscriptionBaseTransitionData((SubscriptionBaseTransitionData) prev, EventType.API_USER, ApiEventType.UNCANCEL, seqId); return withSeq; } @@ -583,7 +583,7 @@ public void rebuildTransitions(final List inputEvents, fi case API_USER: final ApiEvent userEV = (ApiEvent) cur; - apiEventType = userEV.getEventType(); + apiEventType = userEV.getApiEventType(); isFromDisk = userEV.isFromDisk(); switch (apiEventType) { @@ -617,7 +617,7 @@ public void rebuildTransitions(final List inputEvents, fi default: throw new SubscriptionBaseError(String.format( "Unexpected UserEvent type = %s", userEV - .getEventType().toString())); + .getApiEventType().toString())); } break; default: diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java index 9601adfc5e..2e829f30f7 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java @@ -238,7 +238,6 @@ public boolean uncancel(final DefaultSubscriptionBase subscription, final CallCo final SubscriptionBaseEvent uncancelEvent = new ApiEventUncancel(new ApiEventBuilder() .setSubscriptionId(subscription.getId()) .setActiveVersion(subscription.getActiveVersion()) - .setProcessedDate(now) .setRequestedDate(now) .setEffectiveDate(now) .setFromDisk(true)); @@ -355,6 +354,9 @@ private DateTime doChangePlan(final DefaultSubscriptionBase subscription, final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(overrides, context); final Plan newPlan = catalogService.getFullCatalog(internalCallContext).createOrFindPlan(newProductName, newBillingPeriod, newPriceList, overridesWithContext, effectiveDate, subscription.getStartDate()); + if (newPlan.getProduct().getCategory() != subscription.getCategory()) { + throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_INVALID, subscription.getId()); + } final List changeEvents = getEventsOnChangePlan(subscription, newPlan, newPriceList, now, effectiveDate, now, false, internalCallContext); dao.changePlan(subscription, changeEvents, internalCallContext); subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), catalogService.getFullCatalog(internalCallContext)); @@ -380,7 +382,6 @@ public List getEventsOnCreation(final UUID bundleId, fina .setEventPlanPhase(curAndNextPhases[0].getPhase().getName()) .setEventPriceList(realPriceList) .setActiveVersion(activeVersion) - .setProcessedDate(processedDate) .setEffectiveDate(effectiveDate) .setRequestedDate(requestedDate) .setFromDisk(true); @@ -410,7 +411,6 @@ public List getEventsOnChangePlan(final DefaultSubscripti .setEventPlanPhase(currentTimedPhase.getPhase().getName()) .setEventPriceList(newPriceList) .setActiveVersion(subscription.getActiveVersion()) - .setProcessedDate(processedDate) .setEffectiveDate(effectiveDate) .setRequestedDate(requestedDate) .setFromDisk(true)); @@ -443,7 +443,6 @@ public List getEventsOnCancelPlan(final DefaultSubscripti final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder() .setSubscriptionId(subscription.getId()) .setActiveVersion(subscription.getActiveVersion()) - .setProcessedDate(processedDate) .setEffectiveDate(effectiveDate) .setRequestedDate(requestedDate) .setFromDisk(true)); @@ -495,7 +494,6 @@ private List addCancellationAddOnForEventsIfRequired(fi final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder() .setSubscriptionId(cur.getId()) .setActiveVersion(cur.getActiveVersion()) - .setProcessedDate(processedDate) .setEffectiveDate(effectiveDate) .setRequestedDate(requestedDate) .setFromDisk(true)); diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java index 03e813e600..63d5d56883 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java @@ -46,14 +46,11 @@ import org.killbill.billing.entitlement.api.SubscriptionApiException; import org.killbill.billing.entity.EntityPersistenceException; import org.killbill.billing.events.EffectiveSubscriptionInternalEvent; -import org.killbill.billing.events.RepairSubscriptionInternalEvent; import org.killbill.billing.subscription.api.SubscriptionBase; import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; import org.killbill.billing.subscription.api.migration.AccountMigrationData; import org.killbill.billing.subscription.api.migration.AccountMigrationData.BundleMigrationData; import org.killbill.billing.subscription.api.migration.AccountMigrationData.SubscriptionMigrationData; -import org.killbill.billing.subscription.api.timeline.DefaultRepairSubscriptionEvent; -import org.killbill.billing.subscription.api.timeline.SubscriptionDataRepair; import org.killbill.billing.subscription.api.transfer.TransferCancelData; import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEvent; import org.killbill.billing.subscription.api.user.DefaultRequestedSubscriptionEvent; @@ -68,9 +65,11 @@ import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao; import org.killbill.billing.subscription.engine.dao.model.SubscriptionEventModelDao; import org.killbill.billing.subscription.engine.dao.model.SubscriptionModelDao; +import org.killbill.billing.subscription.events.EventBaseBuilder; import org.killbill.billing.subscription.events.SubscriptionBaseEvent; import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType; import org.killbill.billing.subscription.events.phase.PhaseEvent; +import org.killbill.billing.subscription.events.phase.PhaseEventBuilder; import org.killbill.billing.subscription.events.user.ApiEvent; import org.killbill.billing.subscription.events.user.ApiEventBuilder; import org.killbill.billing.subscription.events.user.ApiEventCancel; @@ -676,7 +675,7 @@ final List reinsertFutureMigrateBillingEventOnChangeFromT } if (cur.getEffectiveDate().compareTo(migrateBillingEvent.getEffectiveDate()) > 0) { - if (cur.getType() == EventType.API_USER && ((ApiEvent) cur).getEventType() == ApiEventType.CHANGE) { + if (cur.getType() == EventType.API_USER && ((ApiEvent) cur).getApiEventType() == ApiEventType.CHANGE) { // This is an EOT change that is occurring after the MigrateBilling : returns same list return changeEvents; } @@ -693,7 +692,7 @@ final List reinsertFutureMigrateBillingEventOnChangeFromT final DateTime now = clock.getUTCNow(); final ApiEventBuilder builder = new ApiEventBuilder() .setActive(true) - .setEventType(ApiEventType.MIGRATE_BILLING) + .setApiEventType(ApiEventType.MIGRATE_BILLING) .setFromDisk(true) .setTotalOrdering(migrateBillingEvent.getTotalOrdering()) .setUuid(UUIDs.randomUUID()) @@ -702,7 +701,6 @@ final List reinsertFutureMigrateBillingEventOnChangeFromT .setUpdatedDate(now) .setRequestedDate(migrateBillingEvent.getRequestedDate()) .setEffectiveDate(migrateBillingEvent.getEffectiveDate()) - .setProcessedDate(now) .setActiveVersion(migrateBillingEvent.getCurrentVersion()) .setEventPlan(prevPlan) .setEventPlanPhase(prevPhase) @@ -908,7 +906,6 @@ public int compare(final SubscriptionBase o1, final SubscriptionBase o2) { final SubscriptionBaseEvent addOnCancelEvent = new ApiEventCancel(new ApiEventBuilder() .setSubscriptionId(reloaded.getId()) .setActiveVersion(((DefaultSubscriptionBase) reloaded).getActiveVersion()) - .setProcessedDate(now) .setEffectiveDate(baseTriggerEventForAddOnCancellation.getEffectiveDate()) .setRequestedDate(now) .setCreatedDate(baseTriggerEventForAddOnCancellation.getCreatedDate()) @@ -946,11 +943,19 @@ private void mergeDryRunEvents(final UUID subscriptionId, final List inRepair, final InternalCallContext context) { - transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper() { - @Override - public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception { - final SubscriptionSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionSqlDao.class); - - final SubscriptionEventSqlDao transEventDao = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class); - for (final SubscriptionDataRepair cur : inRepair) { - transactional.updateForRepair(cur.getId().toString(), cur.getActiveVersion(), cur.getAlignStartDate().toDate(), cur.getBundleStartDate().toDate(), context); - for (final SubscriptionBaseEvent event : cur.getInitialEvents()) { - transEventDao.updateVersion(event.getId().toString(), event.getActiveVersion(), context); - } - for (final SubscriptionBaseEvent event : cur.getNewEvents()) { - transEventDao.create(new SubscriptionEventModelDao(event), context); - if (event.getEffectiveDate().isAfter(clock.getUTCNow())) { - recordFutureNotificationFromTransaction(entitySqlDaoWrapperFactory, - event.getEffectiveDate(), - new SubscriptionNotificationKey(event.getId()), - context); - } - } - } - - try { - // Note: we don't send a requested change event here, but a repair event - final RepairSubscriptionInternalEvent busEvent = new DefaultRepairSubscriptionEvent(accountId, bundleId, clock.getUTCNow(), - context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()); - eventBus.postFromTransaction(busEvent, entitySqlDaoWrapperFactory.getHandle().getConnection()); - } catch (EventBusException e) { - log.warn("Failed to post repair subscription event for bundle " + bundleId, e); - } - - return null; - } - }); - } - @Override public void transfer(final UUID srcAccountId, final UUID destAccountId, final BundleMigrationData bundleTransferData, final List transferCancelData, final InternalCallContext fromContext, final InternalCallContext toContext) { diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/RepairSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/RepairSubscriptionDao.java deleted file mode 100644 index 622513460d..0000000000 --- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/RepairSubscriptionDao.java +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.subscription.engine.dao; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.UUID; - -import javax.inject.Inject; - -import org.skife.jdbi.v2.IDBI; - -import org.killbill.billing.ErrorCode; -import org.killbill.billing.callcontext.InternalCallContext; -import org.killbill.billing.callcontext.InternalTenantContext; -import org.killbill.clock.Clock; -import org.killbill.billing.entitlement.api.SubscriptionApiException; -import org.killbill.billing.subscription.api.SubscriptionBase; -import org.killbill.billing.subscription.api.migration.AccountMigrationData; -import org.killbill.billing.subscription.api.migration.AccountMigrationData.BundleMigrationData; -import org.killbill.billing.subscription.api.timeline.RepairSubscriptionLifecycleDao; -import org.killbill.billing.subscription.api.timeline.SubscriptionDataRepair; -import org.killbill.billing.subscription.api.transfer.TransferCancelData; -import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase; -import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle; -import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle; -import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao; -import org.killbill.billing.subscription.events.SubscriptionBaseEvent; -import org.killbill.billing.subscription.exceptions.SubscriptionBaseError; -import org.killbill.billing.util.cache.CacheControllerDispatcher; -import org.killbill.billing.util.dao.NonEntityDao; -import org.killbill.billing.util.entity.Pagination; -import org.killbill.billing.util.entity.dao.EntityDaoBase; -import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper; - -import com.google.common.base.Function; -import com.google.common.collect.Collections2; - -public class RepairSubscriptionDao extends EntityDaoBase implements SubscriptionDao, RepairSubscriptionLifecycleDao { - - private static final String NOT_IMPLEMENTED = "Not implemented"; - - private final ThreadLocal> preThreadsInRepairSubscriptions = new ThreadLocal>(); - - @Inject - public RepairSubscriptionDao(final IDBI dbi, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) { - super(new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao), BundleSqlDao.class); - } - - @Override - protected SubscriptionApiException generateAlreadyExistsException(final SubscriptionBundleModelDao entity, final InternalCallContext context) { - return new SubscriptionApiException(ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS, entity.getExternalKey()); - } - - private static final class SubscriptionEventWithOrderingId { - - private final SubscriptionBaseEvent event; - private final long orderingId; - - public SubscriptionEventWithOrderingId(final SubscriptionBaseEvent event, final long orderingId) { - this.event = event; - this.orderingId = orderingId; - } - - public SubscriptionBaseEvent getEvent() { - return event; - } - - public long getOrderingId() { - return orderingId; - } - - @Override - public String toString() { - final StringBuilder tmp = new StringBuilder(); - tmp.append("["); - tmp.append(event.getType()); - tmp.append(": effDate="); - tmp.append(event.getEffectiveDate()); - tmp.append(", subId="); - tmp.append(event.getSubscriptionId()); - tmp.append(", ordering="); - tmp.append(event.getTotalOrdering()); - tmp.append("]"); - return tmp.toString(); - } - } - - private static final class SubscriptionRepairEvent { - - private final Set events; - private long curOrderingId; - - public SubscriptionRepairEvent(final List initialEvents) { - this.events = new TreeSet(new Comparator() { - @Override - public int compare(final SubscriptionEventWithOrderingId o1, final SubscriptionEventWithOrderingId o2) { - // Work around jdk7 change: compare(o1, o1) is now invoked when inserting the first element - // See: - // - http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5045147 - // - http://hg.openjdk.java.net/jdk7/tl/jdk/rev/bf37edb38fbb - if (o1 == o2) { - return 0; - } - - final int result = o1.getEvent().getEffectiveDate().compareTo(o2.getEvent().getEffectiveDate()); - if (result == 0) { - if (o1.getOrderingId() < o2.getOrderingId()) { - return -1; - } else if (o1.getOrderingId() > o2.getOrderingId()) { - return 1; - } else { - throw new RuntimeException(String.format(" Repair subscription events should not have the same orderingId %s, %s ", o1, o2)); - } - } - return result; - } - }); - - this.curOrderingId = 0; - - if (initialEvents != null) { - addEvents(initialEvents); - } - } - - public List getEvents() { - return new ArrayList(Collections2.transform(events, new Function() { - @Override - public SubscriptionBaseEvent apply(SubscriptionEventWithOrderingId in) { - return in.getEvent(); - } - })); - } - - public void addEvents(final List newEvents) { - for (final SubscriptionBaseEvent cur : newEvents) { - events.add(new SubscriptionEventWithOrderingId(cur, curOrderingId++)); - } - } - } - - private Map getRepairMap() { - if (preThreadsInRepairSubscriptions.get() == null) { - preThreadsInRepairSubscriptions.set(new HashMap()); - } - return preThreadsInRepairSubscriptions.get(); - } - - private SubscriptionRepairEvent getRepairSubscriptionEvents(final UUID subscriptionId) { - final Map map = getRepairMap(); - return map.get(subscriptionId); - } - - @Override - public List getEventsForSubscription(final UUID subscriptionId, final InternalTenantContext context) { - final SubscriptionRepairEvent target = getRepairSubscriptionEvents(subscriptionId); - return new LinkedList(target.getEvents()); - } - - @Override - public void createSubscription(final DefaultSubscriptionBase subscription, final List createEvents, final InternalCallContext context) { - addEvents(subscription.getId(), createEvents); - } - - @Override - public void recreateSubscription(final DefaultSubscriptionBase subscription, final List recreateEvents, final InternalCallContext context) { - addEvents(subscription.getId(), recreateEvents); - } - - @Override - public void cancelSubscription(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent cancelEvent, final InternalCallContext context, final int cancelSeq) { - final UUID subscriptionId = subscription.getId(); - final long activeVersion = cancelEvent.getActiveVersion(); - addEvents(subscriptionId, Collections.singletonList(cancelEvent)); - final SubscriptionRepairEvent target = getRepairSubscriptionEvents(subscriptionId); - boolean foundCancelEvent = false; - for (final SubscriptionBaseEvent cur : target.getEvents()) { - if (cur.getId().equals(cancelEvent.getId())) { - foundCancelEvent = true; - } else if (foundCancelEvent) { - cur.setActiveVersion(activeVersion - 1); - } - } - } - - @Override - public void cancelSubscriptions(final List subscriptions, final List cancelEvents, final InternalCallContext context) { - } - - @Override - public void changePlan(final DefaultSubscriptionBase subscription, final List changeEvents, final InternalCallContext context) { - addEvents(subscription.getId(), changeEvents); - } - - @Override - public void initializeRepair(final UUID subscriptionId, final List initialEvents, final InternalTenantContext context) { - final Map map = getRepairMap(); - if (map.get(subscriptionId) == null) { - final SubscriptionRepairEvent value = new SubscriptionRepairEvent(initialEvents); - map.put(subscriptionId, value); - } else { - throw new SubscriptionBaseError(String.format("Unexpected SubscriptionRepairEvent %s for thread %s", subscriptionId, Thread.currentThread().getName())); - } - } - - @Override - public void cleanup(final InternalTenantContext context) { - final Map map = getRepairMap(); - map.clear(); - } - - private void addEvents(final UUID subscriptionId, final List events) { - final SubscriptionRepairEvent target = getRepairSubscriptionEvents(subscriptionId); - target.addEvents(events); - } - - @Override - public void uncancelSubscription(final DefaultSubscriptionBase subscription, final List uncancelEvents, final InternalCallContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public List getSubscriptionBundleForAccount(final UUID accountId, final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public SubscriptionBaseBundle getSubscriptionBundleFromId(final UUID bundleId, final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public SubscriptionBaseBundle createSubscriptionBundle(final DefaultSubscriptionBaseBundle bundle, final InternalCallContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public SubscriptionBase getSubscriptionFromId(final UUID subscriptionId, final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public UUID getAccountIdFromSubscriptionId(final UUID subscriptionId, final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public SubscriptionBase getBaseSubscription(final UUID bundleId, final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public List getSubscriptions(final UUID bundleId, final List dryRunEvents, final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public void updateChargedThroughDate(final DefaultSubscriptionBase subscription, final InternalCallContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public void createNextPhaseEvent(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent nextPhase, final InternalCallContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public SubscriptionBaseEvent getEventById(final UUID eventId, final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public Map> getSubscriptionsForAccount(final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public Iterable getFutureEventsForAccount(final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public List getPendingEventsForSubscription(final UUID subscriptionId, final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public void migrate(final UUID accountId, final AccountMigrationData data, final InternalCallContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public void repair(final UUID accountId, final UUID bundleId, final List inRepair, final InternalCallContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public void transfer(final UUID srcAccountId, final UUID destAccountId, final BundleMigrationData data, - final List transferCancelData, final InternalCallContext fromContext, - final InternalCallContext toContext) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public void updateBundleExternalKey(final UUID bundleId, final String externalKey, final InternalCallContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public List getSubscriptionBundlesForKey(final String bundleKey, final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public Pagination searchSubscriptionBundles(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public List getNonAOSubscriptionIdsForKey(final String bundleKey, final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } - - @Override - public List getSubscriptionBundlesForAccountAndKey(final UUID accountId, final String bundleKey, final InternalTenantContext context) { - throw new SubscriptionBaseError(NOT_IMPLEMENTED); - } -} diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java index 5503b991af..13cfa2b7b3 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java @@ -27,7 +27,6 @@ import org.killbill.billing.subscription.api.SubscriptionBase; import org.killbill.billing.subscription.api.migration.AccountMigrationData; import org.killbill.billing.subscription.api.migration.AccountMigrationData.BundleMigrationData; -import org.killbill.billing.subscription.api.timeline.SubscriptionDataRepair; import org.killbill.billing.subscription.api.transfer.TransferCancelData; import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase; import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle; @@ -99,8 +98,5 @@ public interface SubscriptionDao extends EntityDao inRepair, InternalCallContext context); - } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java index 2809ea8678..58d75affcc 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java @@ -39,7 +39,6 @@ import org.killbill.billing.subscription.events.user.ApiEventUncancel; import org.killbill.billing.subscription.exceptions.SubscriptionBaseError; import org.killbill.billing.util.dao.TableName; -import org.killbill.billing.entity.EntityBase; import org.killbill.billing.util.entity.dao.EntityModelDao; import org.killbill.billing.util.entity.dao.EntityModelDaoBase; @@ -83,7 +82,7 @@ public SubscriptionEventModelDao(final SubscriptionBaseEvent src) { super(src.getId(), src.getCreatedDate(), src.getUpdatedDate()); this.totalOrdering = src.getTotalOrdering(); this.eventType = src.getType(); - this.userType = eventType == EventType.API_USER ? ((ApiEvent) src).getEventType() : null; + this.userType = eventType == EventType.API_USER ? ((ApiEvent) src).getApiEventType() : null; this.requestedDate = src.getRequestedDate(); this.effectiveDate = src.getEffectiveDate(); this.subscriptionId = src.getSubscriptionId(); @@ -203,40 +202,21 @@ public static SubscriptionBaseEvent toSubscriptionEvent(final SubscriptionEventM .setUpdatedDate(src.getUpdatedDate()) .setRequestedDate(src.getRequestedDate()) .setEffectiveDate(src.getEffectiveDate()) - .setProcessedDate(src.getCreatedDate()) .setActiveVersion(src.getCurrentVersion()) .setActive(src.isActive()); - SubscriptionBaseEvent result = null; + SubscriptionBaseEvent result; if (src.getEventType() == EventType.PHASE) { - result = new PhaseEventData(new PhaseEventBuilder(base).setPhaseName(src.getPhaseName())); + result = (new PhaseEventBuilder(base).setPhaseName(src.getPhaseName())).build(); } else if (src.getEventType() == EventType.API_USER) { final ApiEventBuilder builder = new ApiEventBuilder(base) .setEventPlan(src.getPlanName()) .setEventPlanPhase(src.getPhaseName()) .setEventPriceList(src.getPriceListName()) - .setEventType(src.getUserType()) + .setApiEventType(src.getUserType()) + .setApiEventType(src.getUserType()) .setFromDisk(true); - - if (src.getUserType() == ApiEventType.CREATE) { - result = new ApiEventCreate(builder); - } else if (src.getUserType() == ApiEventType.RE_CREATE) { - result = new ApiEventReCreate(builder); - } else if (src.getUserType() == ApiEventType.MIGRATE_ENTITLEMENT) { - result = new ApiEventMigrateSubscription(builder); - } else if (src.getUserType() == ApiEventType.MIGRATE_BILLING) { - result = new ApiEventMigrateBilling(builder); - } else if (src.getUserType() == ApiEventType.TRANSFER) { - result = new ApiEventTransfer(builder); - } else if (src.getUserType() == ApiEventType.CHANGE) { - result = new ApiEventChange(builder); - } else if (src.getUserType() == ApiEventType.CANCEL) { - result = new ApiEventCancel(builder); - } else if (src.getUserType() == ApiEventType.RE_CREATE) { - result = new ApiEventReCreate(builder); - } else if (src.getUserType() == ApiEventType.UNCANCEL) { - result = new ApiEventUncancel(builder); - } + result = builder.build(); } else { throw new SubscriptionBaseError(String.format("Can't figure out event %s", src.getEventType())); } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/EventBase.java b/subscription/src/main/java/org/killbill/billing/subscription/events/EventBase.java index d80cfc1600..35fdd1ee64 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/EventBase.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/EventBase.java @@ -30,11 +30,10 @@ public abstract class EventBase implements SubscriptionBaseEvent { private final DateTime updatedDate; private final DateTime requestedDate; private final DateTime effectiveDate; - private final DateTime processedDate; - private long totalOrdering; - private long activeVersion; - private boolean isActive; + private final long totalOrdering; + private final long activeVersion; + private final boolean isActive; public EventBase(final EventBaseBuilder builder) { this.totalOrdering = builder.getTotalOrdering(); @@ -44,7 +43,6 @@ public EventBase(final EventBaseBuilder builder) { this.updatedDate = builder.getUpdatedDate(); this.requestedDate = builder.getRequestedDate(); this.effectiveDate = builder.getEffectiveDate(); - this.processedDate = builder.getProcessedDate(); this.activeVersion = builder.getActiveVersion(); this.isActive = builder.isActive(); } @@ -59,11 +57,6 @@ public DateTime getEffectiveDate() { return effectiveDate; } - @Override - public DateTime getProcessedDate() { - return processedDate; - } - @Override public UUID getSubscriptionId() { return subscriptionId; @@ -94,31 +87,11 @@ public long getActiveVersion() { return activeVersion; } - @Override - public void setActiveVersion(final long activeVersion) { - this.activeVersion = activeVersion; - } - @Override public boolean isActive() { return isActive; } - @Override - public void deactivate() { - this.isActive = false; - } - - @Override - public void reactivate() { - this.isActive = true; - } - - @Override - public void setTotalOrdering(final long totalOrdering) { - this.totalOrdering = totalOrdering; - } - // // Really used for unit tests only as the sql implementation relies on date first and then event insertion // @@ -138,10 +111,6 @@ public int compareTo(final SubscriptionBaseEvent other) { return -1; } else if (effectiveDate.isAfter(other.getEffectiveDate())) { return 1; - } else if (processedDate.isBefore(other.getProcessedDate())) { - return -1; - } else if (processedDate.isAfter(other.getProcessedDate())) { - return 1; } else if (requestedDate.isBefore(other.getRequestedDate())) { return -1; } else if (requestedDate.isAfter(other.getRequestedDate())) { @@ -149,7 +118,7 @@ public int compareTo(final SubscriptionBaseEvent other) { } else if (getType() != other.getType()) { return (getType() == EventType.PHASE) ? -1 : 1; } else if (getType() == EventType.API_USER) { - return ((ApiEvent) this).getEventType().compareTo(((ApiEvent) other).getEventType()); + return ((ApiEvent) this).getApiEventType().compareTo(((ApiEvent) other).getApiEventType()); } else { return uuid.compareTo(other.getId()); } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/EventBaseBuilder.java b/subscription/src/main/java/org/killbill/billing/subscription/events/EventBaseBuilder.java index 1c90547a53..7487b8a8f4 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/EventBaseBuilder.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/EventBaseBuilder.java @@ -22,7 +22,7 @@ import org.killbill.billing.util.UUIDs; @SuppressWarnings("unchecked") -public class EventBaseBuilder> { +public abstract class EventBaseBuilder> { private long totalOrdering; private UUID uuid; @@ -31,7 +31,6 @@ public class EventBaseBuilder> { private DateTime updatedDate; private DateTime requestedDate; private DateTime effectiveDate; - private DateTime processedDate; private long activeVersion; private boolean isActive; @@ -41,13 +40,26 @@ public EventBaseBuilder() { this.isActive = true; } + + public EventBaseBuilder(final SubscriptionBaseEvent event) { + this.uuid = event.getId(); + this.subscriptionId = event.getSubscriptionId(); + this.requestedDate = event.getRequestedDate(); + this.effectiveDate = event.getEffectiveDate(); + this.createdDate = event.getCreatedDate(); + this.updatedDate = event.getUpdatedDate(); + this.activeVersion = event.getActiveVersion(); + this.isActive = event.isActive(); + this.totalOrdering = event.getTotalOrdering(); + } + public EventBaseBuilder(final EventBaseBuilder copy) { this.uuid = copy.uuid; this.subscriptionId = copy.subscriptionId; this.requestedDate = copy.requestedDate; this.effectiveDate = copy.effectiveDate; - this.processedDate = copy.processedDate; this.createdDate = copy.getCreatedDate(); + this.updatedDate = copy.getUpdatedDate(); this.activeVersion = copy.activeVersion; this.isActive = copy.isActive; this.totalOrdering = copy.totalOrdering; @@ -88,11 +100,6 @@ public T setEffectiveDate(final DateTime effectiveDate) { return (T) this; } - public T setProcessedDate(final DateTime processedDate) { - this.processedDate = processedDate; - return (T) this; - } - public T setActiveVersion(final long activeVersion) { this.activeVersion = activeVersion; return (T) this; @@ -131,10 +138,6 @@ public DateTime getEffectiveDate() { return effectiveDate; } - public DateTime getProcessedDate() { - return processedDate; - } - public long getActiveVersion() { return activeVersion; } @@ -142,4 +145,6 @@ public long getActiveVersion() { public boolean isActive() { return isActive; } + + public abstract SubscriptionBaseEvent build(); } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java index ceb0211cd6..c610aa041f 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java @@ -34,20 +34,10 @@ public enum EventType { public long getTotalOrdering(); - public void setTotalOrdering(long totalOrdering); - public long getActiveVersion(); - public void setActiveVersion(long activeVersion); - public boolean isActive(); - public void deactivate(); - - public void reactivate(); - - public DateTime getProcessedDate(); - public DateTime getRequestedDate(); public DateTime getEffectiveDate(); diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventBuilder.java b/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventBuilder.java index 21f22e87c6..10f676f1ca 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventBuilder.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventBuilder.java @@ -27,6 +27,11 @@ public PhaseEventBuilder() { super(); } + public PhaseEventBuilder(final PhaseEvent phaseEvent) { + super(phaseEvent); + this.phaseName = phaseEvent.getPhase(); + } + public PhaseEventBuilder(final EventBaseBuilder base) { super(base); } @@ -39,4 +44,8 @@ public PhaseEventBuilder setPhaseName(final String phaseName) { public String getPhaseName() { return phaseName; } + + public PhaseEvent build() { + return new PhaseEventData(this); + } } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java b/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java index b3da3d8aaa..6da067cc43 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java @@ -53,7 +53,6 @@ public String toString() { + ", getRequestedDate()=" + getRequestedDate() + ", getEffectiveDate()=" + getEffectiveDate() + ", getActiveVersion()=" + getActiveVersion() - + ", getProcessedDate()=" + getProcessedDate() + ", getSubscriptionId()=" + getSubscriptionId() + ", isActive()=" + isActive() + "]\n"; } @@ -65,7 +64,6 @@ public static PhaseEvent createNextPhaseEvent(final UUID subscriptionId, final l .setSubscriptionId(subscriptionId) .setRequestedDate(now) .setEffectiveDate(effectiveDate) - .setProcessedDate(now) .setActiveVersion(activeVersion) .setPhaseName(phaseName)); } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEvent.java index a1bf77fa44..ea1338735a 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEvent.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEvent.java @@ -25,7 +25,7 @@ public interface ApiEvent extends SubscriptionBaseEvent { public String getEventPlanPhase(); - public ApiEventType getEventType(); + public ApiEventType getApiEventType(); public String getPriceList(); diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBase.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBase.java index ce462fe125..be40564af0 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBase.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBase.java @@ -20,7 +20,7 @@ public class ApiEventBase extends EventBase implements ApiEvent { - private final ApiEventType eventType; + private final ApiEventType apiEventType; // Only valid for CREATE/CHANGE private final String eventPlan; private final String eventPlanPhase; @@ -29,7 +29,7 @@ public class ApiEventBase extends EventBase implements ApiEvent { public ApiEventBase(final ApiEventBuilder builder) { super(builder); - this.eventType = builder.getEventType(); + this.apiEventType = builder.getApiEventType(); this.eventPriceList = builder.getEventPriceList(); this.eventPlan = builder.getEventPlan(); this.eventPlanPhase = builder.getEventPlanPhase(); @@ -37,8 +37,8 @@ public ApiEventBase(final ApiEventBuilder builder) { } @Override - public ApiEventType getEventType() { - return eventType; + public ApiEventType getApiEventType() { + return apiEventType; } @Override @@ -70,17 +70,16 @@ public boolean isFromDisk() { @Override public String toString() { return "ApiEventBase [ getId()= " + getId() - + " eventType=" + eventType + + " apiEventType=" + apiEventType + ", eventPlan=" + eventPlan + ", eventPlanPhase=" + eventPlanPhase - + ", getEventType()=" + getEventType() + + ", getApiEventType()=" + getApiEventType() + ", getEventPlan()=" + getEventPlan() + ", getEventPlanPhase()=" + getEventPlanPhase() + ", getType()=" + getType() + ", getRequestedDate()=" + getRequestedDate() + ", getEffectiveDate()=" + getEffectiveDate() + ", getActiveVersion()=" + getActiveVersion() - + ", getProcessedDate()=" + getProcessedDate() + ", getSubscriptionId()=" + getSubscriptionId() + ", isActive()=" + isActive() + "]"; } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBuilder.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBuilder.java index 2568e767ad..18cedb6048 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBuilder.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBuilder.java @@ -18,26 +18,33 @@ import org.killbill.billing.subscription.events.EventBaseBuilder; - public class ApiEventBuilder extends EventBaseBuilder { - private ApiEventType eventType; + private ApiEventType apiEventType; private String eventPlan; private String eventPlanPhase; private String eventPriceList; private boolean fromDisk; - public ApiEventBuilder() { super(); } + public ApiEventBuilder(final ApiEvent apiEvent) { + super(apiEvent); + this.apiEventType = apiEvent.getApiEventType(); + this.eventPlan = apiEvent.getEventPlan(); + this.eventPlanPhase = apiEvent.getEventPlanPhase(); + this.eventPriceList = apiEvent.getPriceList(); + this.fromDisk = apiEvent.isFromDisk(); + } + public ApiEventBuilder(final EventBaseBuilder base) { super(base); } - public ApiEventType getEventType() { - return eventType; + public ApiEventType getApiEventType() { + return apiEventType; } public String getEventPlan() { @@ -61,8 +68,8 @@ public ApiEventBuilder setFromDisk(final boolean fromDisk) { return this; } - public ApiEventBuilder setEventType(final ApiEventType eventType) { - this.eventType = eventType; + public ApiEventBuilder setApiEventType(final ApiEventType eventType) { + this.apiEventType = eventType; return this; } @@ -80,4 +87,30 @@ public ApiEventBuilder setEventPriceList(final String eventPriceList) { this.eventPriceList = eventPriceList; return this; } + + public ApiEventBase build() { + final ApiEventBase result; + if (apiEventType == ApiEventType.CREATE) { + result = new ApiEventCreate(this); + } else if (apiEventType == ApiEventType.RE_CREATE) { + result = new ApiEventReCreate(this); + } else if (apiEventType == ApiEventType.MIGRATE_ENTITLEMENT) { + result = new ApiEventMigrateSubscription(this); + } else if (apiEventType == ApiEventType.MIGRATE_BILLING) { + result = new ApiEventMigrateBilling(this); + } else if (apiEventType == ApiEventType.TRANSFER) { + result = new ApiEventTransfer(this); + } else if (apiEventType == ApiEventType.CHANGE) { + result = new ApiEventChange(this); + } else if (apiEventType == ApiEventType.CANCEL) { + result = new ApiEventCancel(this); + } else if (apiEventType == ApiEventType.RE_CREATE) { + result = new ApiEventReCreate(this); + } else if (apiEventType == ApiEventType.UNCANCEL) { + result = new ApiEventUncancel(this); + } else { + throw new IllegalStateException("Unknown ApiEventType " + apiEventType); + } + return result; + } } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCancel.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCancel.java index 5634f72ca8..55c40e8c93 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCancel.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCancel.java @@ -20,6 +20,6 @@ public class ApiEventCancel extends ApiEventBase { public ApiEventCancel(final ApiEventBuilder builder) { - super(builder.setEventType(ApiEventType.CANCEL)); + super(builder.setApiEventType(ApiEventType.CANCEL)); } } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventChange.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventChange.java index dd440fc59c..05b5878e0f 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventChange.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventChange.java @@ -20,6 +20,6 @@ public class ApiEventChange extends ApiEventBase { public ApiEventChange(final ApiEventBuilder builder) { - super(builder.setEventType(ApiEventType.CHANGE)); + super(builder.setApiEventType(ApiEventType.CHANGE)); } } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCreate.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCreate.java index 84d856e8b2..e1b068c195 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCreate.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCreate.java @@ -20,6 +20,6 @@ public class ApiEventCreate extends ApiEventBase { public ApiEventCreate(final ApiEventBuilder builder) { - super(builder.setEventType(ApiEventType.CREATE)); + super(builder.setApiEventType(ApiEventType.CREATE)); } } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateBilling.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateBilling.java index 8cdd287f28..cc1e3a8b0b 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateBilling.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateBilling.java @@ -16,25 +16,8 @@ package org.killbill.billing.subscription.events.user; -import org.joda.time.DateTime; - public class ApiEventMigrateBilling extends ApiEventBase { public ApiEventMigrateBilling(final ApiEventBuilder builder) { - super(builder.setEventType(ApiEventType.MIGRATE_BILLING)); - } - - public ApiEventMigrateBilling(final ApiEventMigrateSubscription input, final DateTime ctd) { - super(new ApiEventBuilder() - .setSubscriptionId(input.getSubscriptionId()) - .setEventPlan(input.getEventPlan()) - .setEventPlanPhase(input.getEventPlanPhase()) - .setEventPriceList(input.getPriceList()) - .setActiveVersion(input.getActiveVersion()) - .setEffectiveDate(ctd) - .setProcessedDate(input.getProcessedDate()) - .setRequestedDate(input.getRequestedDate()) - .setFromDisk(true) - .setEventType(ApiEventType.MIGRATE_BILLING)); + super(builder.setApiEventType(ApiEventType.MIGRATE_BILLING)); } - } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateSubscription.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateSubscription.java index 7594d91675..6194af2682 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateSubscription.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateSubscription.java @@ -19,6 +19,6 @@ public class ApiEventMigrateSubscription extends ApiEventBase { public ApiEventMigrateSubscription(final ApiEventBuilder builder) { - super(builder.setEventType(ApiEventType.MIGRATE_ENTITLEMENT)); + super(builder.setApiEventType(ApiEventType.MIGRATE_ENTITLEMENT)); } } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventReCreate.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventReCreate.java index 2b1e02530c..76fe64b704 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventReCreate.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventReCreate.java @@ -19,6 +19,6 @@ public class ApiEventReCreate extends ApiEventBase { public ApiEventReCreate(final ApiEventBuilder builder) { - super(builder.setEventType(ApiEventType.RE_CREATE)); + super(builder.setApiEventType(ApiEventType.RE_CREATE)); } } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventTransfer.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventTransfer.java index d432b0ae1c..a304b72c08 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventTransfer.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventTransfer.java @@ -17,7 +17,7 @@ public class ApiEventTransfer extends ApiEventBase { public ApiEventTransfer(final ApiEventBuilder builder) { - super(builder.setEventType(ApiEventType.TRANSFER)); + super(builder.setApiEventType(ApiEventType.TRANSFER)); } } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventUncancel.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventUncancel.java index 5e56d95f95..5f3755c0bb 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventUncancel.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventUncancel.java @@ -19,6 +19,6 @@ public class ApiEventUncancel extends ApiEventBase { public ApiEventUncancel(final ApiEventBuilder builder) { - super(builder.setEventType(ApiEventType.UNCANCEL)); + super(builder.setApiEventType(ApiEventType.UNCANCEL)); } } diff --git a/subscription/src/main/java/org/killbill/billing/subscription/glue/DefaultSubscriptionModule.java b/subscription/src/main/java/org/killbill/billing/subscription/glue/DefaultSubscriptionModule.java index 01531ea727..e0ba5ab3e3 100644 --- a/subscription/src/main/java/org/killbill/billing/subscription/glue/DefaultSubscriptionModule.java +++ b/subscription/src/main/java/org/killbill/billing/subscription/glue/DefaultSubscriptionModule.java @@ -29,8 +29,6 @@ import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi; import org.killbill.billing.subscription.api.svcs.DefaultSubscriptionInternalApi; import org.killbill.billing.subscription.api.timeline.DefaultSubscriptionBaseTimelineApi; -import org.killbill.billing.subscription.api.timeline.RepairSubscriptionApiService; -import org.killbill.billing.subscription.api.timeline.RepairSubscriptionLifecycleDao; import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimelineApi; import org.killbill.billing.subscription.api.transfer.DefaultSubscriptionBaseTransferApi; import org.killbill.billing.subscription.api.transfer.SubscriptionBaseTransferApi; @@ -38,18 +36,13 @@ import org.killbill.billing.subscription.engine.addon.AddonUtils; import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseService; import org.killbill.billing.subscription.engine.dao.DefaultSubscriptionDao; -import org.killbill.billing.subscription.engine.dao.RepairSubscriptionDao; import org.killbill.billing.subscription.engine.dao.SubscriptionDao; import org.killbill.billing.util.config.SubscriptionConfig; import org.killbill.billing.util.glue.KillBillModule; import org.skife.config.ConfigurationObjectFactory; -import com.google.inject.name.Names; - public class DefaultSubscriptionModule extends KillBillModule implements SubscriptionModule { - public static final String REPAIR_NAMED = "repair"; - public DefaultSubscriptionModule(final KillbillConfigSource configSource) { super(configSource); } @@ -61,15 +54,10 @@ protected void installConfig() { protected void installSubscriptionDao() { bind(SubscriptionDao.class).to(DefaultSubscriptionDao.class).asEagerSingleton(); - bind(SubscriptionDao.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionDao.class); - bind(RepairSubscriptionLifecycleDao.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionDao.class); - bind(RepairSubscriptionDao.class).asEagerSingleton(); } protected void installSubscriptionCore() { - bind(SubscriptionBaseApiService.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionApiService.class).asEagerSingleton(); bind(SubscriptionBaseApiService.class).to(DefaultSubscriptionBaseApiService.class).asEagerSingleton(); - bind(DefaultSubscriptionBaseService.class).asEagerSingleton(); bind(PlanAligner.class).asEagerSingleton(); bind(AddonUtils.class).asEagerSingleton(); diff --git a/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java b/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java index 75172fffd1..4d7a3e6b31 100644 --- a/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java +++ b/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java @@ -223,7 +223,7 @@ private SubscriptionBaseEvent createSubscriptionEvent(final DateTime effectiveDa eventBuilder.setFromDisk(true); eventBuilder.setActiveVersion(activeVersion); - return new ApiEventBase(eventBuilder.setEventType(apiEventType)); + return new ApiEventBase(eventBuilder.setApiEventType(apiEventType)); } private TimedPhase getNextTimedPhaseOnChange(final DefaultSubscriptionBase defaultSubscriptionBase, diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java b/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java index 2121dc0e34..11417821f0 100644 --- a/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java +++ b/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java @@ -24,10 +24,8 @@ import org.killbill.billing.GuicyKillbillTestSuiteNoDB; import org.killbill.billing.entitlement.api.Entitlement.EntitlementState; -import org.killbill.billing.subscription.api.timeline.DefaultRepairSubscriptionEvent; import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEvent; import org.killbill.billing.events.EffectiveSubscriptionInternalEvent; -import org.killbill.billing.events.RepairSubscriptionInternalEvent; import org.killbill.billing.util.jackson.ObjectMapper; public class TestEventJson extends GuicyKillbillTestSuiteNoDB { @@ -47,15 +45,4 @@ public void testSubscriptionEvent() throws Exception { final Object obj = mapper.readValue(json, claz); Assert.assertTrue(obj.equals(e)); } - - @Test(groups = "fast") - public void testRepairSubscriptionEvent() throws Exception { - final RepairSubscriptionInternalEvent e = new DefaultRepairSubscriptionEvent(UUID.randomUUID(), UUID.randomUUID(), new DateTime(), 1L, 2L, null); - - final String json = mapper.writeValueAsString(e); - - final Class claz = Class.forName(DefaultRepairSubscriptionEvent.class.getName()); - final Object obj = mapper.readValue(json, claz); - Assert.assertTrue(obj.equals(e)); - } } diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairBP.java b/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairBP.java deleted file mode 100644 index 49601b604e..0000000000 --- a/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairBP.java +++ /dev/null @@ -1,702 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.subscription.api.timeline; - -import java.util.Collections; -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.annotations.Test; - -import org.killbill.billing.ErrorCode; -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.Plan; -import org.killbill.billing.catalog.api.PlanPhase; -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.Entitlement.EntitlementState; -import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB; -import org.killbill.billing.subscription.api.SubscriptionBase; -import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; -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.SubscriptionBaseApiException; -import org.killbill.billing.subscription.api.user.SubscriptionEvents; -import org.killbill.billing.subscription.api.user.TestSubscriptionHelper.TestWithException; -import org.killbill.billing.subscription.api.user.TestSubscriptionHelper.TestWithExceptionCallback; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertNull; -import static org.testng.Assert.assertTrue; - -public class TestRepairBP extends SubscriptionTestSuiteWithEmbeddedDB { - - @Test(groups = "slow") - public void testFetchBundleRepair() throws Exception { - final String baseProduct = "Shotgun"; - final BillingPeriod baseTerm = BillingPeriod.MONTHLY; - final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME; - - // CREATE BP - final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList); - - final String aoProduct = "Telescopic-Scope"; - final BillingPeriod aoTerm = BillingPeriod.MONTHLY; - final String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME; - - final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, aoProduct, aoTerm, aoPriceList); - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - final List subscriptionRepair = bundleRepair.getSubscriptions(); - assertEquals(subscriptionRepair.size(), 2); - - for (final SubscriptionBaseTimeline cur : subscriptionRepair) { - assertNull(cur.getDeletedEvents()); - assertNull(cur.getNewEvents()); - - final List events = cur.getExistingEvents(); - assertEquals(events.size(), 2); - testUtil.sortExistingEvent(events); - - assertEquals(events.get(0).getSubscriptionTransitionType(), SubscriptionBaseTransitionType.CREATE); - assertEquals(events.get(1).getSubscriptionTransitionType(), SubscriptionBaseTransitionType.PHASE); - final boolean isBP = cur.getId().equals(baseSubscription.getId()); - if (isBP) { - assertEquals(cur.getId(), baseSubscription.getId()); - - assertEquals(events.get(0).getPlanPhaseSpecifier().getProductName(), baseProduct); - assertEquals(events.get(0).getPlanPhaseSpecifier().getPhaseType(), PhaseType.TRIAL); - assertEquals(events.get(0).getPlanPhaseSpecifier().getProductCategory(), ProductCategory.BASE); - assertEquals(events.get(0).getPlanPhaseSpecifier().getPriceListName(), basePriceList); - assertEquals(events.get(0).getPlanPhaseSpecifier().getBillingPeriod(), BillingPeriod.NO_BILLING_PERIOD); - - assertEquals(events.get(1).getPlanPhaseSpecifier().getProductName(), baseProduct); - assertEquals(events.get(1).getPlanPhaseSpecifier().getPhaseType(), PhaseType.EVERGREEN); - assertEquals(events.get(1).getPlanPhaseSpecifier().getProductCategory(), ProductCategory.BASE); - assertEquals(events.get(1).getPlanPhaseSpecifier().getPriceListName(), basePriceList); - assertEquals(events.get(1).getPlanPhaseSpecifier().getBillingPeriod(), baseTerm); - } else { - assertEquals(cur.getId(), aoSubscription.getId()); - - assertEquals(events.get(0).getPlanPhaseSpecifier().getProductName(), aoProduct); - assertEquals(events.get(0).getPlanPhaseSpecifier().getPhaseType(), PhaseType.DISCOUNT); - assertEquals(events.get(0).getPlanPhaseSpecifier().getProductCategory(), ProductCategory.ADD_ON); - assertEquals(events.get(0).getPlanPhaseSpecifier().getPriceListName(), aoPriceList); - assertEquals(events.get(1).getPlanPhaseSpecifier().getBillingPeriod(), aoTerm); - - assertEquals(events.get(1).getPlanPhaseSpecifier().getProductName(), aoProduct); - assertEquals(events.get(1).getPlanPhaseSpecifier().getPhaseType(), PhaseType.EVERGREEN); - assertEquals(events.get(1).getPlanPhaseSpecifier().getProductCategory(), ProductCategory.ADD_ON); - assertEquals(events.get(1).getPlanPhaseSpecifier().getPriceListName(), aoPriceList); - assertEquals(events.get(1).getPlanPhaseSpecifier().getBillingPeriod(), aoTerm); - } - } - assertListenerStatus(); - } - - @Test(groups = "slow") - public void testBPRepairWithCancellationOnstart() throws Exception { - final String baseProduct = "Shotgun"; - final DateTime startDate = clock.getUTCNow(); - - // CREATE BP - final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate); - - // Stays in trial-- for instance - final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(10)); - clock.addDeltaFromReality(it.toDurationMillis()); - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - final List des = new LinkedList(); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId())); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CANCEL, baseSubscription.getStartDate(), null); - - final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne)); - - // FIRST ISSUE DRY RUN - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair)); - - boolean dryRun = true; - final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext); - testUtil.sortEventsOnBundle(dryRunBundleRepair); - List subscriptionRepair = dryRunBundleRepair.getSubscriptions(); - assertEquals(subscriptionRepair.size(), 1); - SubscriptionBaseTimeline cur = subscriptionRepair.get(0); - int index = 0; - final List events = subscriptionRepair.get(0).getExistingEvents(); - assertEquals(events.size(), 2); - final List expected = new LinkedList(); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, baseProduct, PhaseType.TRIAL, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, baseSubscription.getStartDate())); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CANCEL, baseProduct, PhaseType.TRIAL, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, baseSubscription.getStartDate())); - - for (final ExistingEvent e : expected) { - testUtil.validateExistingEventForAssertion(e, events.get(index++)); - } - - final DefaultSubscriptionBase dryRunBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - - assertEquals(dryRunBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - assertEquals(dryRunBaseSubscription.getBundleId(), bundle.getId()); - assertEquals(dryRunBaseSubscription.getStartDate(), baseSubscription.getStartDate()); - - final Plan currentPlan = dryRunBaseSubscription.getCurrentPlan(); - assertNotNull(currentPlan); - assertEquals(currentPlan.getProduct().getName(), baseProduct); - assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE); - assertEquals(currentPlan.getRecurringBillingPeriod(), BillingPeriod.MONTHLY); - - final PlanPhase currentPhase = dryRunBaseSubscription.getCurrentPhase(); - assertNotNull(currentPhase); - assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL); - - // SECOND RE-ISSUE CALL-- NON DRY RUN - dryRun = false; - testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE); - final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext); - assertListenerStatus(); - - subscriptionRepair = realRunBundleRepair.getSubscriptions(); - assertEquals(subscriptionRepair.size(), 1); - cur = subscriptionRepair.get(0); - assertEquals(cur.getId(), baseSubscription.getId()); - index = 0; - for (final ExistingEvent e : expected) { - testUtil.validateExistingEventForAssertion(e, events.get(index++)); - } - final DefaultSubscriptionBase realRunBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - assertEquals(realRunBaseSubscription.getAllTransitions().size(), 2); - - assertEquals(realRunBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - assertEquals(realRunBaseSubscription.getBundleId(), bundle.getId()); - assertEquals(realRunBaseSubscription.getStartDate(), startDate); - - assertEquals(realRunBaseSubscription.getState(), EntitlementState.CANCELLED); - - assertListenerStatus(); - } - - @Test(groups = "slow") - public void testBPRepairReplaceCreateBeforeTrial() throws Exception { - final String baseProduct = "Shotgun"; - final String newBaseProduct = "Assault-Rifle"; - - final DateTime startDate = clock.getUTCNow(); - final int clockShift = -1; - final DateTime restartDate = startDate.plusDays(clockShift).minusDays(1); - final LinkedList expected = new LinkedList(); - - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, newBaseProduct, PhaseType.TRIAL, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, restartDate)); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, newBaseProduct, PhaseType.EVERGREEN, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, restartDate.plusDays(30))); - - testBPRepairCreate(true, startDate, clockShift, baseProduct, newBaseProduct, expected); - assertListenerStatus(); - } - - @Test(groups = "slow") - public void testBPRepairReplaceCreateInTrial() throws Exception { - final String baseProduct = "Shotgun"; - final String newBaseProduct = "Assault-Rifle"; - - final DateTime startDate = clock.getUTCNow(); - final int clockShift = 10; - final DateTime restartDate = startDate.plusDays(clockShift).minusDays(1); - final LinkedList expected = new LinkedList(); - - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, newBaseProduct, PhaseType.TRIAL, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, restartDate)); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, newBaseProduct, PhaseType.EVERGREEN, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, restartDate.plusDays(30))); - - final UUID baseSubscriptionId = testBPRepairCreate(true, startDate, clockShift, baseProduct, newBaseProduct, expected); - - testListener.pushExpectedEvent(NextEvent.PHASE); - final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(32)); - clock.addDeltaFromReality(it.toDurationMillis()); - assertListenerStatus(); - - // CHECK WHAT"S GOING ON AFTER WE MOVE CLOCK-- FUTURE MOTIFICATION SHOULD KICK IN - final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscriptionId, internalCallContext); - - assertEquals(subscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - assertEquals(subscription.getBundleId(), bundle.getId()); - assertEquals(subscription.getStartDate(), restartDate); - assertEquals(subscription.getBundleStartDate(), restartDate); - - final Plan currentPlan = subscription.getCurrentPlan(); - assertNotNull(currentPlan); - assertEquals(currentPlan.getProduct().getName(), newBaseProduct); - assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE); - assertEquals(currentPlan.getRecurringBillingPeriod(), BillingPeriod.MONTHLY); - - final PlanPhase currentPhase = subscription.getCurrentPhase(); - assertNotNull(currentPhase); - assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN); - - assertListenerStatus(); - } - - @Test(groups = "slow") - public void testBPRepairReplaceCreateAfterTrial() throws Exception { - final String baseProduct = "Shotgun"; - final String newBaseProduct = "Assault-Rifle"; - - final DateTime startDate = clock.getUTCNow(); - final int clockShift = 40; - final DateTime restartDate = startDate.plusDays(clockShift).minusDays(1); - final LinkedList expected = new LinkedList(); - - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, newBaseProduct, PhaseType.TRIAL, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, restartDate)); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, newBaseProduct, PhaseType.EVERGREEN, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, restartDate.plusDays(30))); - - testBPRepairCreate(false, startDate, clockShift, baseProduct, newBaseProduct, expected); - assertListenerStatus(); - } - - private UUID testBPRepairCreate(final boolean inTrial, final DateTime startDate, final int clockShift, - final String baseProduct, final String newBaseProduct, final List expectedEvents) throws Exception { - // CREATE BP - final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate); - - // MOVE CLOCK - if (clockShift > 0) { - if (!inTrial) { - testListener.pushExpectedEvent(NextEvent.PHASE); - } - - final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(clockShift)); - clock.addDeltaFromReality(it.toDurationMillis()); - if (!inTrial) { - assertListenerStatus(); - } - } - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - final DateTime newCreateTime = baseSubscription.getStartDate().plusDays(clockShift - 1); - - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(newBaseProduct, ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL); - - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CREATE, newCreateTime, spec); - final List des = new LinkedList(); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(0).getEventId())); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId())); - - final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne)); - - // FIRST ISSUE DRY RUN - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair)); - - boolean dryRun = true; - final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext); - List subscriptionRepair = dryRunBundleRepair.getSubscriptions(); - assertEquals(subscriptionRepair.size(), 1); - SubscriptionBaseTimeline cur = subscriptionRepair.get(0); - assertEquals(cur.getId(), baseSubscription.getId()); - - List events = cur.getExistingEvents(); - assertEquals(expectedEvents.size(), events.size()); - int index = 0; - for (final ExistingEvent e : expectedEvents) { - testUtil.validateExistingEventForAssertion(e, events.get(index++)); - } - final DefaultSubscriptionBase dryRunBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - - assertEquals(dryRunBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - assertEquals(dryRunBaseSubscription.getBundleId(), bundle.getId()); - assertTrue(dryRunBaseSubscription.getStartDate().compareTo(baseSubscription.getStartDate()) == 0); - - Plan currentPlan = dryRunBaseSubscription.getCurrentPlan(); - assertNotNull(currentPlan); - assertEquals(currentPlan.getProduct().getName(), baseProduct); - assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE); - assertEquals(currentPlan.getRecurringBillingPeriod(), BillingPeriod.MONTHLY); - - PlanPhase currentPhase = dryRunBaseSubscription.getCurrentPhase(); - assertNotNull(currentPhase); - if (inTrial) { - assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL); - } else { - assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN); - } - - // SECOND RE-ISSUE CALL-- NON DRY RUN - dryRun = false; - testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE); - final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext); - assertListenerStatus(); - subscriptionRepair = realRunBundleRepair.getSubscriptions(); - assertEquals(subscriptionRepair.size(), 1); - cur = subscriptionRepair.get(0); - assertEquals(cur.getId(), baseSubscription.getId()); - - events = cur.getExistingEvents(); - for (final ExistingEvent e : events) { - log.info(String.format("%s, %s, %s, %s", e.getSubscriptionTransitionType(), e.getEffectiveDate(), e.getPlanPhaseSpecifier().getProductName(), e.getPlanPhaseSpecifier().getPhaseType())); - } - assertEquals(events.size(), expectedEvents.size()); - index = 0; - for (final ExistingEvent e : expectedEvents) { - testUtil.validateExistingEventForAssertion(e, events.get(index++)); - } - final DefaultSubscriptionBase realRunBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - assertEquals(realRunBaseSubscription.getAllTransitions().size(), 2); - - assertEquals(realRunBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - assertEquals(realRunBaseSubscription.getBundleId(), bundle.getId()); - assertEquals(realRunBaseSubscription.getStartDate(), newCreateTime); - - currentPlan = realRunBaseSubscription.getCurrentPlan(); - assertNotNull(currentPlan); - assertEquals(currentPlan.getProduct().getName(), newBaseProduct); - assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE); - assertEquals(currentPlan.getRecurringBillingPeriod(), BillingPeriod.MONTHLY); - - currentPhase = realRunBaseSubscription.getCurrentPhase(); - assertNotNull(currentPhase); - assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL); - - return baseSubscription.getId(); - } - - @Test(groups = "slow") - public void testBPRepairAddChangeInTrial() throws Exception { - final String baseProduct = "Shotgun"; - final String newBaseProduct = "Assault-Rifle"; - - final DateTime startDate = clock.getUTCNow(); - final int clockShift = 10; - final DateTime changeDate = startDate.plusDays(clockShift).minusDays(1); - final LinkedList expected = new LinkedList(); - - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, baseProduct, PhaseType.TRIAL, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, startDate)); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CHANGE, newBaseProduct, PhaseType.TRIAL, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, changeDate)); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, newBaseProduct, PhaseType.EVERGREEN, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, startDate.plusDays(30))); - - final UUID baseSubscriptionId = testBPRepairAddChange(true, startDate, clockShift, baseProduct, newBaseProduct, expected, 3); - - // CHECK WHAT"S GOING ON AFTER WE MOVE CLOCK-- FUTURE MOTIFICATION SHOULD KICK IN - testListener.pushExpectedEvent(NextEvent.PHASE); - final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(32)); - clock.addDeltaFromReality(it.toDurationMillis()); - assertListenerStatus(); - final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscriptionId, internalCallContext); - - assertEquals(subscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - assertEquals(subscription.getBundleId(), bundle.getId()); - assertEquals(subscription.getStartDate(), startDate); - assertEquals(subscription.getBundleStartDate(), startDate); - - final Plan currentPlan = subscription.getCurrentPlan(); - assertNotNull(currentPlan); - assertEquals(currentPlan.getProduct().getName(), newBaseProduct); - assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE); - assertEquals(currentPlan.getRecurringBillingPeriod(), BillingPeriod.MONTHLY); - - final PlanPhase currentPhase = subscription.getCurrentPhase(); - assertNotNull(currentPhase); - assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN); - - assertListenerStatus(); - } - - @Test(groups = "slow") - public void testBPRepairAddChangeAfterTrial() throws Exception { - final String baseProduct = "Shotgun"; - final String newBaseProduct = "Assault-Rifle"; - - final DateTime startDate = clock.getUTCNow(); - final int clockShift = 40; - final DateTime changeDate = startDate.plusDays(clockShift).minusDays(1); - - final LinkedList expected = new LinkedList(); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, baseProduct, PhaseType.TRIAL, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, startDate)); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, baseProduct, PhaseType.EVERGREEN, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, startDate.plusDays(30))); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CHANGE, newBaseProduct, PhaseType.EVERGREEN, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, changeDate)); - testBPRepairAddChange(false, startDate, clockShift, baseProduct, newBaseProduct, expected, 3); - - assertListenerStatus(); - } - - private UUID testBPRepairAddChange(final boolean inTrial, final DateTime startDate, final int clockShift, - final String baseProduct, final String newBaseProduct, final List expectedEvents, final int expectedTransitions) throws Exception { - // CREATE BP - final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate); - - // MOVE CLOCK - if (!inTrial) { - testListener.pushExpectedEvent(NextEvent.PHASE); - } - - final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(clockShift)); - clock.addDeltaFromReality(it.toDurationMillis()); - if (!inTrial) { - assertListenerStatus(); - } - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - final DateTime changeTime = baseSubscription.getStartDate().plusDays(clockShift - 1); - - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(newBaseProduct, ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL); - - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, changeTime, spec); - final List des = new LinkedList(); - if (inTrial) { - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId())); - } - final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne)); - - // FIRST ISSUE DRY RUN - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair)); - - boolean dryRun = true; - final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext); - - List subscriptionRepair = dryRunBundleRepair.getSubscriptions(); - assertEquals(subscriptionRepair.size(), 1); - SubscriptionBaseTimeline cur = subscriptionRepair.get(0); - assertEquals(cur.getId(), baseSubscription.getId()); - - List events = cur.getExistingEvents(); - assertEquals(expectedEvents.size(), events.size()); - int index = 0; - for (final ExistingEvent e : expectedEvents) { - testUtil.validateExistingEventForAssertion(e, events.get(index++)); - } - final DefaultSubscriptionBase dryRunBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - - assertEquals(dryRunBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - assertEquals(dryRunBaseSubscription.getBundleId(), bundle.getId()); - assertEquals(dryRunBaseSubscription.getStartDate(), baseSubscription.getStartDate()); - - Plan currentPlan = dryRunBaseSubscription.getCurrentPlan(); - assertNotNull(currentPlan); - assertEquals(currentPlan.getProduct().getName(), baseProduct); - assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE); - assertEquals(currentPlan.getRecurringBillingPeriod(), BillingPeriod.MONTHLY); - - PlanPhase currentPhase = dryRunBaseSubscription.getCurrentPhase(); - assertNotNull(currentPhase); - if (inTrial) { - assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL); - } else { - assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN); - } - - // SECOND RE-ISSUE CALL-- NON DRY RUN - dryRun = false; - testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE); - final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext); - assertListenerStatus(); - - subscriptionRepair = realRunBundleRepair.getSubscriptions(); - assertEquals(subscriptionRepair.size(), 1); - cur = subscriptionRepair.get(0); - assertEquals(cur.getId(), baseSubscription.getId()); - - events = cur.getExistingEvents(); - assertEquals(expectedEvents.size(), events.size()); - index = 0; - for (final ExistingEvent e : expectedEvents) { - testUtil.validateExistingEventForAssertion(e, events.get(index++)); - } - final DefaultSubscriptionBase realRunBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - assertEquals(realRunBaseSubscription.getAllTransitions().size(), expectedTransitions); - - assertEquals(realRunBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - assertEquals(realRunBaseSubscription.getBundleId(), bundle.getId()); - assertEquals(realRunBaseSubscription.getStartDate(), baseSubscription.getStartDate()); - - currentPlan = realRunBaseSubscription.getCurrentPlan(); - assertNotNull(currentPlan); - assertEquals(currentPlan.getProduct().getName(), newBaseProduct); - assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE); - assertEquals(currentPlan.getRecurringBillingPeriod(), BillingPeriod.MONTHLY); - - currentPhase = realRunBaseSubscription.getCurrentPhase(); - assertNotNull(currentPhase); - if (inTrial) { - assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL); - } else { - assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN); - } - return baseSubscription.getId(); - } - - @Test(groups = "slow") - public void testRepairWithFutureCancelEvent() throws Exception { - final DateTime startDate = clock.getUTCNow(); - - // CREATE BP - SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate); - - // MOVE CLOCK -- OUT OF TRIAL - testListener.pushExpectedEvent(NextEvent.PHASE); - - final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(35)); - clock.addDeltaFromReality(it.toDurationMillis()); - assertListenerStatus(); - - // SET CTD to BASE SUBSCRIPTION SP CANCEL OCCURS EOT - final DateTime newChargedThroughDate = baseSubscription.getStartDate().plusDays(30).plusMonths(1); - subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate, internalCallContext); - baseSubscription = subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - - baseSubscription.changePlan("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, callContext); - - // CHECK CHANGE DID NOT OCCUR YET - Plan currentPlan = baseSubscription.getCurrentPlan(); - assertNotNull(currentPlan); - assertEquals(currentPlan.getProduct().getName(), "Shotgun"); - assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE); - assertEquals(currentPlan.getRecurringBillingPeriod(), BillingPeriod.MONTHLY); - - final DateTime repairTime = clock.getUTCNow().minusDays(1); - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN); - - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, repairTime, spec); - final List des = new LinkedList(); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(2).getEventId())); - - final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne)); - - // SKIP DRY RUN AND DO REPAIR... - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair)); - - final boolean dryRun = false; - testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE); - repairApi.repairBundle(bRepair, dryRun, callContext); - assertListenerStatus(); - - baseSubscription = subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - - assertEquals(((DefaultSubscriptionBase) baseSubscription).getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - assertEquals(baseSubscription.getBundleId(), bundle.getId()); - assertEquals(baseSubscription.getStartDate(), baseSubscription.getStartDate()); - - currentPlan = baseSubscription.getCurrentPlan(); - assertNotNull(currentPlan); - assertEquals(currentPlan.getProduct().getName(), "Assault-Rifle"); - assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE); - assertEquals(currentPlan.getRecurringBillingPeriod(), BillingPeriod.MONTHLY); - - final PlanPhase currentPhase = baseSubscription.getCurrentPhase(); - assertNotNull(currentPhase); - assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN); - - assertListenerStatus(); - } - - // Needs real SQL backend to be tested properly - @Test(groups = "slow") - public void testENT_REPAIR_VIEW_CHANGED_newEvent() throws Exception { - final TestWithException test = new TestWithException(); - final DateTime startDate = clock.getUTCNow(); - - final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate); - - test.withException(new TestWithExceptionCallback() { - @Override - public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException { - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, baseSubscription.getStartDate().plusDays(10), spec); - final List des = new LinkedList(); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(0).getEventId())); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId())); - final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne)); - - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair)); - - testListener.pushExpectedEvent(NextEvent.CHANGE); - final DateTime changeTime = clock.getUTCNow(); - baseSubscription.changePlanWithDate("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, changeTime, callContext); - assertListenerStatus(); - - repairApi.repairBundle(bRepair, true, callContext); - assertListenerStatus(); - } - }, ErrorCode.SUB_REPAIR_VIEW_CHANGED); - } - - @Test(groups = "slow") - public void testENT_REPAIR_VIEW_CHANGED_ctd() throws Exception { - final TestWithException test = new TestWithException(); - final DateTime startDate = clock.getUTCNow(); - - final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate); - - test.withException(new TestWithExceptionCallback() { - @Override - public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException { - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, baseSubscription.getStartDate().plusDays(10), spec); - final List des = new LinkedList(); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(0).getEventId())); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId())); - final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne)); - - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair)); - - final DateTime newChargedThroughDate = baseSubscription.getStartDate().plusDays(30).plusMonths(1); - - // Move clock at least a sec to make sure the last_sys_update from bundle is different-- and therefore generates a different viewId - clock.setDeltaFromReality(1000); - - subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate, internalCallContext); - subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - - repairApi.repairBundle(bRepair, true, callContext); - - assertListenerStatus(); - } - }, ErrorCode.SUB_REPAIR_VIEW_CHANGED); - } -} diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithAO.java b/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithAO.java deleted file mode 100644 index f5d10a99f1..0000000000 --- a/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithAO.java +++ /dev/null @@ -1,726 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.subscription.api.timeline; - -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import org.joda.time.DateTime; -import org.joda.time.Interval; -import org.testng.annotations.Test; - -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.Plan; -import org.killbill.billing.catalog.api.PlanPhase; -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.Entitlement.EntitlementState; -import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB; -import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; -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 TestRepairWithAO extends SubscriptionTestSuiteWithEmbeddedDB { - - @Test(groups = "slow") - public void testRepairChangeBPWithAddonIncluded() throws Exception { - final String baseProduct = "Shotgun"; - final BillingPeriod baseTerm = BillingPeriod.MONTHLY; - final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME; - - // CREATE BP - final DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList); - - // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL - Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4)); - clock.addDeltaFromReality(it.toDurationMillis()); - - final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME); - - final DefaultSubscriptionBase aoSubscription2 = testUtil.createSubscription(bundle, "Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME); - - // MOVE CLOCK A LITTLE BIT MORE -- STILL IN TRIAL - it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3)); - clock.addDeltaFromReality(it.toDurationMillis()); - - BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - // Quick check - SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 2); - - SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - SubscriptionBaseTimeline aoRepair2 = testUtil.getSubscriptionRepair(aoSubscription2.getId(), bundleRepair); - assertEquals(aoRepair2.getExistingEvents().size(), 2); - - final DateTime bpChangeDate = clock.getUTCNow().minusDays(1); - - final List des = new LinkedList(); - des.add(testUtil.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 = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, bpChangeDate, spec); - - bpRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne)); - - bundleRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(bpRepair)); - - boolean dryRun = true; - final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bundleRepair, dryRun, callContext); - - aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), dryRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - aoRepair2 = testUtil.getSubscriptionRepair(aoSubscription2.getId(), dryRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), dryRunBundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 3); - - // Check expected for AO - final List expectedAO = new LinkedList(); - expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.DISCOUNT, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription.getStartDate())); - expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CANCEL, "Telescopic-Scope", PhaseType.DISCOUNT, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, bpChangeDate)); - int index = 0; - for (final ExistingEvent e : expectedAO) { - testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++)); - } - - final List expectedAO2 = new LinkedList(); - expectedAO2.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Laser-Scope", PhaseType.DISCOUNT, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription2.getStartDate())); - expectedAO2.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Laser-Scope", PhaseType.EVERGREEN, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription2.getStartDate().plusMonths(1))); - index = 0; - for (final ExistingEvent e : expectedAO2) { - testUtil.validateExistingEventForAssertion(e, aoRepair2.getExistingEvents().get(index++)); - } - - // Check expected for BP - final List expectedBP = new LinkedList(); - expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Shotgun", PhaseType.TRIAL, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, baseSubscription.getStartDate())); - expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CHANGE, "Assault-Rifle", PhaseType.TRIAL, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, bpChangeDate)); - expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Assault-Rifle", PhaseType.EVERGREEN, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, baseSubscription.getStartDate().plusDays(30))); - index = 0; - for (final ExistingEvent e : expectedBP) { - testUtil.validateExistingEventForAssertion(e, bpRepair.getExistingEvents().get(index++)); - } - - DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newAoSubscription.getAllTransitions().size(), 2); - assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - - DefaultSubscriptionBase newAoSubscription2 = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription2.getId(), internalCallContext); - assertEquals(newAoSubscription2.getState(), EntitlementState.ACTIVE); - assertEquals(newAoSubscription2.getAllTransitions().size(), 2); - assertEquals(newAoSubscription2.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - - DefaultSubscriptionBase newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newBaseSubscription.getAllTransitions().size(), 2); - assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - - dryRun = false; - testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE); - final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bundleRepair, dryRun, callContext); - assertListenerStatus(); - - aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), realRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), realRunBundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 3); - - index = 0; - for (final ExistingEvent e : expectedAO) { - testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++)); - } - - index = 0; - for (final ExistingEvent e : expectedAO2) { - testUtil.validateExistingEventForAssertion(e, aoRepair2.getExistingEvents().get(index++)); - } - - index = 0; - for (final ExistingEvent e : expectedBP) { - testUtil.validateExistingEventForAssertion(e, bpRepair.getExistingEvents().get(index++)); - } - - newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - assertEquals(newAoSubscription.getState(), EntitlementState.CANCELLED); - assertEquals(newAoSubscription.getAllTransitions().size(), 2); - assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - - newAoSubscription2 = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription2.getId(), internalCallContext); - assertEquals(newAoSubscription2.getState(), EntitlementState.ACTIVE); - assertEquals(newAoSubscription2.getAllTransitions().size(), 2); - assertEquals(newAoSubscription2.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - - newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newBaseSubscription.getAllTransitions().size(), 3); - assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - } - - @Test(groups = "slow") - public void testRepairChangeBPWithAddonNonAvailable() throws Exception { - final String baseProduct = "Shotgun"; - final BillingPeriod baseTerm = BillingPeriod.MONTHLY; - final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME; - - // CREATE BP - final DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList); - - // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL - Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3)); - clock.addDeltaFromReality(it.toDurationMillis()); - - final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME); - - // MOVE CLOCK A LITTLE BIT MORE -- AFTER TRIAL - testListener.pushExpectedEvent(NextEvent.PHASE); - testListener.pushExpectedEvent(NextEvent.PHASE); - - it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(32)); - clock.addDeltaFromReality(it.toDurationMillis()); - assertListenerStatus(); - - BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - // Quick check - SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 2); - - SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - final DateTime bpChangeDate = clock.getUTCNow().minusDays(1); - - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, bpChangeDate, spec); - - bpRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), Collections.emptyList(), Collections.singletonList(ne)); - - bundleRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(bpRepair)); - - boolean dryRun = true; - final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bundleRepair, dryRun, callContext); - - aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), dryRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 3); - - bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), dryRunBundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 3); - - // Check expected for AO - final List expectedAO = new LinkedList(); - expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.DISCOUNT, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription.getStartDate())); - expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.EVERGREEN, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, baseSubscription.getStartDate().plusMonths(1))); - expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CANCEL, "Telescopic-Scope", PhaseType.EVERGREEN, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, bpChangeDate)); - int index = 0; - for (final ExistingEvent e : expectedAO) { - testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++)); - } - - // Check expected for BP - final List expectedBP = new LinkedList(); - expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Shotgun", PhaseType.TRIAL, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, baseSubscription.getStartDate())); - expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Shotgun", PhaseType.EVERGREEN, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, baseSubscription.getStartDate().plusDays(30))); - expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CHANGE, "Pistol", PhaseType.EVERGREEN, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, bpChangeDate)); - index = 0; - for (final ExistingEvent e : expectedBP) { - testUtil.validateExistingEventForAssertion(e, bpRepair.getExistingEvents().get(index++)); - } - - DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newAoSubscription.getAllTransitions().size(), 2); - assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - - DefaultSubscriptionBase newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newBaseSubscription.getAllTransitions().size(), 2); - assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - - dryRun = false; - testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE); - final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bundleRepair, dryRun, callContext); - assertListenerStatus(); - - aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), realRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 3); - - bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), realRunBundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 3); - - index = 0; - for (final ExistingEvent e : expectedAO) { - testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++)); - } - - index = 0; - for (final ExistingEvent e : expectedBP) { - testUtil.validateExistingEventForAssertion(e, bpRepair.getExistingEvents().get(index++)); - } - - newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - assertEquals(newAoSubscription.getState(), EntitlementState.CANCELLED); - assertEquals(newAoSubscription.getAllTransitions().size(), 3); - assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - - newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newBaseSubscription.getAllTransitions().size(), 3); - assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - } - - @Test(groups = "slow") - public void testRepairCancelBP_EOT_WithAddons() throws Exception { - final String baseProduct = "Shotgun"; - final BillingPeriod baseTerm = BillingPeriod.MONTHLY; - final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME; - - // CREATE BP - DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList); - - // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL - Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4)); - clock.addDeltaFromReality(it.toDurationMillis()); - - final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME); - - // MOVE CLOCK A LITTLE BIT MORE -- AFTER TRIAL - testListener.pushExpectedEvent(NextEvent.PHASE); - testListener.pushExpectedEvent(NextEvent.PHASE); - - it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(40)); - clock.addDeltaFromReality(it.toDurationMillis()); - assertListenerStatus(); - - // SET CTD to BASE SUBSCRIPTION SP CANCEL OCCURS EOT - final DateTime newChargedThroughDate = baseSubscription.getStartDate().plusDays(30).plusMonths(1); - subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate, internalCallContext); - baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - - BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - // Quick check - SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 2); - - SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - final DateTime bpCancelDate = clock.getUTCNow().minusDays(1); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CANCEL, bpCancelDate, null); - bpRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), Collections.emptyList(), Collections.singletonList(ne)); - bundleRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(bpRepair)); - - boolean dryRun = true; - final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bundleRepair, dryRun, callContext); - - aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), dryRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 3); - - bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), dryRunBundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 3); - - // Check expected for AO - final List expectedAO = new LinkedList(); - expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.DISCOUNT, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription.getStartDate())); - expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Telescopic-Scope", PhaseType.EVERGREEN, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, baseSubscription.getStartDate().plusMonths(1))); - expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CANCEL, "Telescopic-Scope", PhaseType.EVERGREEN, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, bpCancelDate)); - - int index = 0; - for (final ExistingEvent e : expectedAO) { - testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++)); - } - - // Check expected for BP - final List expectedBP = new LinkedList(); - expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Shotgun", PhaseType.TRIAL, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, baseSubscription.getStartDate())); - expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Shotgun", PhaseType.EVERGREEN, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, baseSubscription.getStartDate().plusDays(30))); - expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CANCEL, "Shotgun", PhaseType.EVERGREEN, - ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, bpCancelDate)); - index = 0; - for (final ExistingEvent e : expectedBP) { - testUtil.validateExistingEventForAssertion(e, bpRepair.getExistingEvents().get(index++)); - } - - DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newAoSubscription.getAllTransitions().size(), 2); - assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - - DefaultSubscriptionBase newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newBaseSubscription.getAllTransitions().size(), 2); - assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - - dryRun = false; - testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE); - final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bundleRepair, dryRun, callContext); - assertListenerStatus(); - - aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), realRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 3); - - bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), realRunBundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 3); - - index = 0; - for (final ExistingEvent e : expectedAO) { - testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++)); - } - - index = 0; - for (final ExistingEvent e : expectedBP) { - testUtil.validateExistingEventForAssertion(e, bpRepair.getExistingEvents().get(index++)); - } - - newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - assertEquals(newAoSubscription.getState(), EntitlementState.CANCELLED); - assertEquals(newAoSubscription.getAllTransitions().size(), 3); - assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - - newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - assertEquals(newBaseSubscription.getState(), EntitlementState.CANCELLED); - assertEquals(newBaseSubscription.getAllTransitions().size(), 3); - assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - - } - - @Test(groups = "slow") - public void testRepairCancelAO() throws Exception { - final String baseProduct = "Shotgun"; - final BillingPeriod baseTerm = BillingPeriod.MONTHLY; - final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME; - - // CREATE BP - final DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList); - - // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL - Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4)); - clock.addDeltaFromReality(it.toDurationMillis()); - - final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME); - - // MOVE CLOCK A LITTLE BIT MORE -- STILL IN TRIAL - it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3)); - clock.addDeltaFromReality(it.toDurationMillis()); - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - // Quick check - SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 2); - - SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - final List des = new LinkedList(); - des.add(testUtil.createDeletedEvent(aoRepair.getExistingEvents().get(1).getEventId())); - final DateTime aoCancelDate = aoSubscription.getStartDate().plusDays(1); - - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CANCEL, aoCancelDate, null); - - final SubscriptionBaseTimeline saoRepair = testUtil.createSubscriptionRepair(aoSubscription.getId(), des, Collections.singletonList(ne)); - - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(saoRepair)); - - boolean dryRun = true; - final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext); - - aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), dryRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 2); - - final List expected = new LinkedList(); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.DISCOUNT, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription.getStartDate())); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CANCEL, "Telescopic-Scope", PhaseType.DISCOUNT, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoCancelDate)); - int index = 0; - for (final ExistingEvent e : expected) { - testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++)); - } - DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newAoSubscription.getAllTransitions().size(), 2); - assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - - DefaultSubscriptionBase newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newBaseSubscription.getAllTransitions().size(), 2); - assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - - dryRun = false; - testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE); - final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext); - assertListenerStatus(); - - aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), realRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - index = 0; - for (final ExistingEvent e : expected) { - testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++)); - } - - newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - assertEquals(newAoSubscription.getState(), EntitlementState.CANCELLED); - assertEquals(newAoSubscription.getAllTransitions().size(), 2); - assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - - newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext); - assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newBaseSubscription.getAllTransitions().size(), 2); - assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - } - - @Test(groups = "slow") - public void testRepairRecreateAO() throws Exception { - final String baseProduct = "Shotgun"; - final BillingPeriod baseTerm = BillingPeriod.MONTHLY; - final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME; - - // CREATE BP - final DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList); - - // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL - Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4)); - clock.addDeltaFromReality(it.toDurationMillis()); - - final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME); - - // MOVE CLOCK A LITTLE BIT MORE -- STILL IN TRIAL - it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3)); - clock.addDeltaFromReality(it.toDurationMillis()); - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - // Quick check - final SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 2); - - SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - final List des = new LinkedList(); - des.add(testUtil.createDeletedEvent(aoRepair.getExistingEvents().get(0).getEventId())); - des.add(testUtil.createDeletedEvent(aoRepair.getExistingEvents().get(1).getEventId())); - - final DateTime aoRecreateDate = aoSubscription.getStartDate().plusDays(1); - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.DISCOUNT); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CREATE, aoRecreateDate, spec); - - final SubscriptionBaseTimeline saoRepair = testUtil.createSubscriptionRepair(aoSubscription.getId(), des, Collections.singletonList(ne)); - - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(saoRepair)); - - boolean dryRun = true; - final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext); - - aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), dryRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - final List expected = new LinkedList(); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.DISCOUNT, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoRecreateDate)); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Telescopic-Scope", PhaseType.EVERGREEN, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, baseSubscription.getStartDate().plusMonths(1) /* Bundle align */)); - int index = 0; - for (final ExistingEvent e : expected) { - testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++)); - } - DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newAoSubscription.getAllTransitions().size(), 2); - assertEquals(newAoSubscription.getStartDate(), aoSubscription.getStartDate()); - assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION); - - // NOW COMMIT - dryRun = false; - testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE); - final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext); - assertListenerStatus(); - - aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), realRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - index = 0; - for (final ExistingEvent e : expected) { - testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++)); - } - - newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newAoSubscription.getAllTransitions().size(), 2); - assertEquals(newAoSubscription.getStartDate(), aoRecreateDate); - assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - - } - - // Fasten your seatbelt here: - // - // We are doing repair for multi-phase tiered-addon with different alignment: - // Telescopic-Scope -> Laser-Scope - // Tiered ADON logic - // . Both multi phase - // . Telescopic-Scope (bundle align) and Laser-Scope is SubscriptionBase align - // - @Test(groups = "slow") - public void testRepairChangeAOOK() throws Exception { - final String baseProduct = "Shotgun"; - final BillingPeriod baseTerm = BillingPeriod.MONTHLY; - final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME; - - // CREATE BP - final DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList); - - // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL - Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4)); - clock.addDeltaFromReality(it.toDurationMillis()); - - final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME); - - // MOVE CLOCK A LITTLE BIT MORE -- STILL IN TRIAL - it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3)); - clock.addDeltaFromReality(it.toDurationMillis()); - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - // Quick check - final SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 2); - - SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - final List des = new LinkedList(); - des.add(testUtil.createDeletedEvent(aoRepair.getExistingEvents().get(1).getEventId())); - final DateTime aoChangeDate = aoSubscription.getStartDate().plusDays(1); - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Laser-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, aoChangeDate, spec); - - final SubscriptionBaseTimeline saoRepair = testUtil.createSubscriptionRepair(aoSubscription.getId(), des, Collections.singletonList(ne)); - - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(saoRepair)); - - boolean dryRun = true; - final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext); - - aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), dryRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 3); - - final List expected = new LinkedList(); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.DISCOUNT, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription.getStartDate())); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CHANGE, "Laser-Scope", PhaseType.DISCOUNT, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoChangeDate)); - expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Laser-Scope", PhaseType.EVERGREEN, - ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, - aoSubscription.getStartDate().plusMonths(1) /* SubscriptionBase alignment */)); - - int index = 0; - for (final ExistingEvent e : expected) { - testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++)); - } - DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newAoSubscription.getAllTransitions().size(), 2); - - // AND NOW COMMIT - dryRun = false; - testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE); - final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext); - assertListenerStatus(); - - aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), realRunBundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 3); - index = 0; - for (final ExistingEvent e : expected) { - testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++)); - } - - newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE); - assertEquals(newAoSubscription.getAllTransitions().size(), 3); - - assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1); - assertEquals(newAoSubscription.getBundleId(), bundle.getId()); - assertEquals(newAoSubscription.getStartDate(), aoSubscription.getStartDate()); - - final Plan currentPlan = newAoSubscription.getCurrentPlan(); - assertNotNull(currentPlan); - assertEquals(currentPlan.getProduct().getName(), "Laser-Scope"); - assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.ADD_ON); - assertEquals(currentPlan.getRecurringBillingPeriod(), BillingPeriod.MONTHLY); - - PlanPhase currentPhase = newAoSubscription.getCurrentPhase(); - assertNotNull(currentPhase); - assertEquals(currentPhase.getPhaseType(), PhaseType.DISCOUNT); - - // One phase for BP an one phase for the new AO (laser-scope) - testListener.pushExpectedEvent(NextEvent.PHASE); - testListener.pushExpectedEvent(NextEvent.PHASE); - - it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(60)); - clock.addDeltaFromReality(it.toDurationMillis()); - assertListenerStatus(); - - newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext); - currentPhase = newAoSubscription.getCurrentPhase(); - assertNotNull(currentPhase); - assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN); - } -} diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithError.java b/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithError.java deleted file mode 100644 index 5b81058aa0..0000000000 --- a/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithError.java +++ /dev/null @@ -1,421 +0,0 @@ -/* - * Copyright 2010-2013 Ning, Inc. - * - * Ning 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.subscription.api.timeline; - -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import org.joda.time.DateTime; -import org.joda.time.Interval; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -import org.killbill.billing.ErrorCode; -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.subscription.SubscriptionTestSuiteNoDB; -import org.killbill.billing.subscription.api.SubscriptionBase; -import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; -import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.DeletedEvent; -import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.NewEvent; -import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase; -import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException; -import org.killbill.billing.subscription.api.user.TestSubscriptionHelper.TestWithException; -import org.killbill.billing.subscription.api.user.TestSubscriptionHelper.TestWithExceptionCallback; -import org.killbill.billing.util.UUIDs; - -import static org.testng.Assert.assertEquals; - -public class TestRepairWithError extends SubscriptionTestSuiteNoDB { - - private static final String baseProduct = "Shotgun"; - private TestWithException test; - private SubscriptionBase baseSubscription; - - @Override - @BeforeMethod(groups = "fast") - public void beforeMethod() throws Exception { - super.beforeMethod(); - test = new TestWithException(); - final DateTime startDate = clock.getUTCNow(); - baseSubscription = testUtil.createSubscription(bundle, baseProduct, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate); - } - - @Test(groups = "fast") - public void testENT_REPAIR_NEW_EVENT_BEFORE_LAST_BP_REMAINING() throws Exception { - test.withException(new TestWithExceptionCallback() { - @Override - public void doTest() throws SubscriptionBaseRepairException { - // MOVE AFTER TRIAL - testListener.pushExpectedEvent(NextEvent.PHASE); - - final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(40)); - clock.addDeltaFromReality(it.toDurationMillis()); - - assertListenerStatus(); - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, baseSubscription.getStartDate().plusDays(10), spec); - - final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), Collections.emptyList(), Collections.singletonList(ne)); - - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair)); - - repairApi.repairBundle(bRepair, true, callContext); - } - }, ErrorCode.SUB_REPAIR_NEW_EVENT_BEFORE_LAST_BP_REMAINING); - } - - @Test(groups = "fast") - public void testENT_REPAIR_INVALID_DELETE_SET() throws Exception { - test.withException(new TestWithExceptionCallback() { - @Override - public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException { - - Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3)); - clock.addDeltaFromReality(it.toDurationMillis()); - - testListener.pushExpectedEvent(NextEvent.CHANGE); - final DateTime changeTime = clock.getUTCNow(); - baseSubscription.changePlanWithDate("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, changeTime, callContext); - assertListenerStatus(); - - // MOVE AFTER TRIAL - testListener.pushExpectedEvent(NextEvent.PHASE); - it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(40)); - clock.addDeltaFromReality(it.toDurationMillis()); - assertListenerStatus(); - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, baseSubscription.getStartDate().plusDays(10), spec); - final DeletedEvent de = testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId()); - - final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), Collections.singletonList(de), Collections.singletonList(ne)); - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair)); - - repairApi.repairBundle(bRepair, true, callContext); - } - }, ErrorCode.SUB_REPAIR_INVALID_DELETE_SET); - } - - @Test(groups = "fast") - public void testENT_REPAIR_NON_EXISTENT_DELETE_EVENT() throws Exception { - test.withException(new TestWithExceptionCallback() { - @Override - public void doTest() throws SubscriptionBaseRepairException { - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, baseSubscription.getStartDate().plusDays(10), spec); - final DeletedEvent de = testUtil.createDeletedEvent(UUIDs.randomUUID()); - final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), Collections.singletonList(de), Collections.singletonList(ne)); - - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair)); - - repairApi.repairBundle(bRepair, true, callContext); - } - }, ErrorCode.SUB_REPAIR_NON_EXISTENT_DELETE_EVENT); - } - - @Test(groups = "fast") - public void testENT_REPAIR_SUB_RECREATE_NOT_EMPTY() throws Exception { - test.withException(new TestWithExceptionCallback() { - @Override - public void doTest() throws SubscriptionBaseRepairException { - - // MOVE AFTER TRIAL - testListener.pushExpectedEvent(NextEvent.PHASE); - final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(40)); - clock.addDeltaFromReality(it.toDurationMillis()); - assertListenerStatus(); - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CREATE, baseSubscription.getStartDate().plusDays(10), spec); - final List des = new LinkedList(); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId())); - final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne)); - - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair)); - - repairApi.repairBundle(bRepair, true, callContext); - - } - }, ErrorCode.SUB_REPAIR_SUB_RECREATE_NOT_EMPTY); - } - - @Test(groups = "fast") - public void testENT_REPAIR_SUB_EMPTY() throws Exception { - test.withException(new TestWithExceptionCallback() { - - @Override - public void doTest() throws SubscriptionBaseRepairException { - - // MOVE AFTER TRIAL - testListener.pushExpectedEvent(NextEvent.PHASE); - final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(40)); - clock.addDeltaFromReality(it.toDurationMillis()); - assertListenerStatus(); - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, baseSubscription.getStartDate().plusDays(10), spec); - final List des = new LinkedList(); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(0).getEventId())); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId())); - final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne)); - - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair)); - - repairApi.repairBundle(bRepair, true, callContext); - } - }, ErrorCode.SUB_REPAIR_SUB_EMPTY); - } - - @Test(groups = "fast") - public void testENT_REPAIR_AO_CREATE_BEFORE_BP_START() throws Exception { - test.withException(new TestWithExceptionCallback() { - @Override - public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException { - // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL - Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4)); - clock.addDeltaFromReality(it.toDurationMillis()); - final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME); - - // MOVE CLOCK A LITTLE BIT MORE -- STILL IN TRIAL - it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4)); - clock.addDeltaFromReality(it.toDurationMillis()); - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - // Quick check - final SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 2); - - final SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - final List des = new LinkedList(); - des.add(testUtil.createDeletedEvent(aoRepair.getExistingEvents().get(0).getEventId())); - des.add(testUtil.createDeletedEvent(aoRepair.getExistingEvents().get(1).getEventId())); - - final DateTime aoRecreateDate = aoSubscription.getStartDate().minusDays(5); - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.DISCOUNT); - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CREATE, aoRecreateDate, spec); - - final SubscriptionBaseTimeline saoRepair = testUtil.createSubscriptionRepair(aoSubscription.getId(), des, Collections.singletonList(ne)); - - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(saoRepair)); - - final boolean dryRun = true; - repairApi.repairBundle(bRepair, dryRun, callContext); - } - }, ErrorCode.SUB_REPAIR_AO_CREATE_BEFORE_BP_START); - } - - @Test(groups = "fast") - public void testENT_REPAIR_NEW_EVENT_BEFORE_LAST_AO_REMAINING() throws Exception { - test.withException(new TestWithExceptionCallback() { - @Override - public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException { - - // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL - Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4)); - clock.addDeltaFromReality(it.toDurationMillis()); - final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME); - - // MOVE CLOCK A LITTLE BIT MORE -- STILL IN TRIAL - it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4)); - clock.addDeltaFromReality(it.toDurationMillis()); - - BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - // Quick check - final SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair); - assertEquals(bpRepair.getExistingEvents().size(), 2); - - final SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair); - assertEquals(aoRepair.getExistingEvents().size(), 2); - - final List des = new LinkedList(); - //des.add(createDeletedEvent(aoRepair.getExistingEvents().get(1).getEventId())); - final DateTime aoCancelDate = aoSubscription.getStartDate().plusDays(10); - - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CANCEL, aoCancelDate, null); - - final SubscriptionBaseTimeline saoRepair = testUtil.createSubscriptionRepair(aoSubscription.getId(), des, Collections.singletonList(ne)); - - bundleRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(saoRepair)); - - final boolean dryRun = true; - repairApi.repairBundle(bundleRepair, dryRun, callContext); - } - }, ErrorCode.SUB_REPAIR_NEW_EVENT_BEFORE_LAST_AO_REMAINING); - } - - @Test(groups = "fast", enabled = false) // TODO - fails on jdk7 on Travis - public void testENT_REPAIR_BP_RECREATE_MISSING_AO() throws Exception { - test.withException(new TestWithExceptionCallback() { - @Override - public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException { - - //testListener.pushExpectedEvent(NextEvent.PHASE); - - final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4)); - clock.addDeltaFromReality(it.toDurationMillis()); - //assertListenerStatus(); - - final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME); - - final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext); - testUtil.sortEventsOnBundle(bundleRepair); - - final DateTime newCreateTime = baseSubscription.getStartDate().plusDays(3); - - final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL); - - final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CREATE, newCreateTime, spec); - final List des = new LinkedList(); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(0).getEventId())); - des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId())); - - final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne)); - - // FIRST ISSUE DRY RUN - final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair)); - - final boolean dryRun = true; - repairApi.repairBundle(bRepair, dryRun, callContext); - } - }, ErrorCode.SUB_REPAIR_BP_RECREATE_MISSING_AO); - } - - // - // CAN'T seem to trigger such case easily, other errors trigger before... - // - @Test(groups = "fast", enabled = false) - public void testENT_REPAIR_BP_RECREATE_MISSING_AO_CREATE() throws Exception { - test.withException(new TestWithExceptionCallback() { - @Override - public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException { - /* - //testListener.pushExpectedEvent(NextEvent.PHASE); - - Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4)); - clock.addDeltaFromReality(it.toDurationMillis()); - - - DefaultSubscriptionBase aoSubscription = createSubscription("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME); - - BundleRepair bundleRepair = repairApi.getBundleRepair(bundle.getId()); - sortEventsOnBundle(bundleRepair); - - DateTime newCreateTime = baseSubscription.getStartDate().plusDays(3); - - PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL); - - NewEvent ne = createNewEvent(SubscriptionBaseTransitionType.CREATE, newCreateTime, spec); - List des = new LinkedList(); - des.add(createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(0).getEventId())); - des.add(createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId())); - - SubscriptionRepair bpRepair = createSubscriptionReapir(baseSubscription.getId(), des, Collections.singletonList(ne)); - - ne = createNewEvent(SubscriptionBaseTransitionType.CANCEL, clock.getUTCNow().minusDays(1), null); - SubscriptionRepair aoRepair = createSubscriptionReapir(aoSubscription.getId(), Collections.emptyList(), Collections.singletonList(ne)); - - - List allRepairs = new LinkedList(); - allRepairs.add(bpRepair); - allRepairs.add(aoRepair); - bundleRepair = createBundleRepair(bundle.getId(), bundleRepair.getViewId(), allRepairs); - // FIRST ISSUE DRY RUN - BundleRepair bRepair = createBundleRepair(bundle.getId(), bundleRepair.getViewId(), allRepairs); - - boolean dryRun = true; - repairApi.repairBundle(bRepair, dryRun, callcontext); - */ - } - }, ErrorCode.SUB_REPAIR_BP_RECREATE_MISSING_AO_CREATE); - } - - @Test(groups = "fast", enabled = false) - public void testENT_REPAIR_MISSING_AO_DELETE_EVENT() throws Exception { - test.withException(new TestWithExceptionCallback() { - @Override - public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException { - - /* - // MOVE CLOCK -- JUST BEFORE END OF TRIAL - * - Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(29)); - clock.addDeltaFromReality(it.toDurationMillis()); - - clock.setDeltaFromReality(getDurationDay(29), 0); - - DefaultSubscriptionBase aoSubscription = createSubscription("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME); - - // MOVE CLOCK -- RIGHT OUT OF TRIAL - testListener.pushExpectedEvent(NextEvent.PHASE); - clock.addDeltaFromReality(getDurationDay(5)); - assertListenerStatus(); - - DateTime requestedChange = clock.getUTCNow(); - baseSubscription.changePlanWithRequestedDate("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, requestedChange, callcontext); - - DateTime reapairTime = clock.getUTCNow().minusDays(1); - - BundleRepair bundleRepair = repairApi.getBundleRepair(bundle.getId()); - sortEventsOnBundle(bundleRepair); - - SubscriptionRepair bpRepair = getSubscriptionRepair(baseSubscription.getId(), bundleRepair); - SubscriptionRepair aoRepair = getSubscriptionRepair(aoSubscription.getId(), bundleRepair); - - List bpdes = new LinkedList(); - bpdes.add(createDeletedEvent(bpRepair.getExistingEvents().get(2).getEventId())); - bpRepair = createSubscriptionReapir(baseSubscription.getId(), bpdes, Collections.emptyList()); - - NewEvent ne = createNewEvent(SubscriptionBaseTransitionType.CANCEL, reapairTime, null); - aoRepair = createSubscriptionReapir(aoSubscription.getId(), Collections.emptyList(), Collections.singletonList(ne)); - - List allRepairs = new LinkedList(); - allRepairs.add(bpRepair); - allRepairs.add(aoRepair); - bundleRepair = createBundleRepair(bundle.getId(), bundleRepair.getViewId(), allRepairs); - - boolean dryRun = false; - repairApi.repairBundle(bundleRepair, dryRun, callcontext); - */ - } - }, ErrorCode.SUB_REPAIR_MISSING_AO_DELETE_EVENT); - } - -} diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java index 02a39dee32..64836345ae 100644 --- a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java +++ b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java @@ -101,7 +101,7 @@ public void testEventsForCancelledSubscriptionAfterTransfer() throws Exception { Assert.assertEquals(events.size(), 1); Assert.assertEquals(events.get(0).getType(), EventType.API_USER); Assert.assertEquals(events.get(0).getEffectiveDate(), transferDate); - Assert.assertEquals(((ApiEventTransfer) events.get(0)).getEventType(), ApiEventType.TRANSFER); + Assert.assertEquals(((ApiEventTransfer) events.get(0)).getApiEventType(), ApiEventType.TRANSFER); } @Test(groups = "fast") @@ -115,7 +115,7 @@ public void testEventsAfterTransferForMigratedBundle1() throws Exception { Assert.assertEquals(events.size(), 1); Assert.assertEquals(events.get(0).getType(), EventType.API_USER); Assert.assertEquals(events.get(0).getEffectiveDate(), transferDate); - Assert.assertEquals(((ApiEventTransfer) events.get(0)).getEventType(), ApiEventType.TRANSFER); + Assert.assertEquals(((ApiEventTransfer) events.get(0)).getApiEventType(), ApiEventType.TRANSFER); } @Test(groups = "fast") @@ -129,7 +129,7 @@ public void testEventsAfterTransferForMigratedBundle2() throws Exception { Assert.assertEquals(events.size(), 1); Assert.assertEquals(events.get(0).getType(), EventType.API_USER); Assert.assertEquals(events.get(0).getEffectiveDate(), transferDate); - Assert.assertEquals(((ApiEventTransfer) events.get(0)).getEventType(), ApiEventType.TRANSFER); + Assert.assertEquals(((ApiEventTransfer) events.get(0)).getApiEventType(), ApiEventType.TRANSFER); } @Test(groups = "fast") @@ -143,7 +143,7 @@ public void testEventsAfterTransferForMigratedBundle3() throws Exception { Assert.assertEquals(events.size(), 1); Assert.assertEquals(events.get(0).getType(), EventType.API_USER); Assert.assertEquals(events.get(0).getEffectiveDate(), transferDate); - Assert.assertEquals(((ApiEventTransfer) events.get(0)).getEventType(), ApiEventType.TRANSFER); + Assert.assertEquals(((ApiEventTransfer) events.get(0)).getApiEventType(), ApiEventType.TRANSFER); } @Test(groups = "fast") @@ -157,7 +157,7 @@ public void testEventsAfterTransferForMigratedBundle4() throws Exception { Assert.assertEquals(events.size(), 1); Assert.assertEquals(events.get(0).getType(), EventType.API_USER); Assert.assertEquals(events.get(0).getEffectiveDate(), migrateSubscriptionEventEffectiveDate); - Assert.assertEquals(((ApiEventTransfer) events.get(0)).getEventType(), ApiEventType.TRANSFER); + Assert.assertEquals(((ApiEventTransfer) events.get(0)).getApiEventType(), ApiEventType.TRANSFER); } private List transferBundle(final DateTime migrateSubscriptionEventEffectiveDate, final DateTime migrateBillingEventEffectiveDate, diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestSubscriptionHelper.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestSubscriptionHelper.java index 0cea1b8750..487a3d9a79 100644 --- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestSubscriptionHelper.java +++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestSubscriptionHelper.java @@ -19,8 +19,6 @@ package org.killbill.billing.subscription.api.user; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.UUID; @@ -30,7 +28,6 @@ import org.joda.time.DateTime; import org.joda.time.Period; -import org.killbill.billing.ErrorCode; import org.killbill.billing.account.api.Account; import org.killbill.billing.account.api.AccountApiException; import org.killbill.billing.account.api.AccountUserApi; @@ -47,17 +44,10 @@ import org.killbill.billing.events.EffectiveSubscriptionInternalEvent; import org.killbill.billing.mock.MockAccountBuilder; import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi; -import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi.AccountMigration; import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi.BundleMigration; import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi.SubscriptionMigration; import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi.SubscriptionMigrationCase; -import org.killbill.billing.subscription.api.timeline.BundleBaseTimeline; -import org.killbill.billing.subscription.api.timeline.SubscriptionBaseRepairException; -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.engine.dao.SubscriptionDao; import org.killbill.billing.subscription.events.SubscriptionBaseEvent; import org.killbill.billing.subscription.events.phase.PhaseEvent; @@ -67,7 +57,6 @@ import org.killbill.clock.Clock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testng.Assert; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; @@ -136,7 +125,7 @@ public void checkNextPhaseChange(final DefaultSubscriptionBase subscription, fin assertEquals(cur.getEffectiveDate(), expPhaseChange); } else if (cur instanceof ApiEvent) { final ApiEvent uEvent = (ApiEvent) cur; - assertEquals(ApiEventType.CHANGE, uEvent.getEventType()); + assertEquals(ApiEventType.CHANGE, uEvent.getApiEventType()); assertEquals(foundChange, false); foundChange = true; } else { @@ -151,31 +140,6 @@ public void assertDateWithin(final DateTime in, final DateTime lower, final Date assertTrue(in.isEqual(upper) || in.isBefore(upper)); } - public Duration getDurationDay(final int days) { - final Duration result = new Duration() { - @Override - public TimeUnit getUnit() { - return TimeUnit.DAYS; - } - - @Override - public int getNumber() { - return days; - } - - @Override - public DateTime addToDateTime(final DateTime dateTime) { - return null; - } - - @Override - public Period toJodaPeriod() { - throw new UnsupportedOperationException(); - } - }; - return result; - } - public Duration getDurationMonth(final int months) { final Duration result = new Duration() { @Override @@ -201,31 +165,6 @@ public Period toJodaPeriod() { return result; } - public Duration getDurationYear(final int years) { - final Duration result = new Duration() { - @Override - public TimeUnit getUnit() { - return TimeUnit.YEARS; - } - - @Override - public int getNumber() { - return years; - } - - @Override - public DateTime addToDateTime(final DateTime dateTime) { - return dateTime.plusYears(years); - } - - @Override - public Period toJodaPeriod() { - throw new UnsupportedOperationException(); - } - }; - return result; - } - public PlanPhaseSpecifier getProductSpecifier(final String productName, final String priceList, final BillingPeriod term, @Nullable final PhaseType phaseType) { @@ -401,206 +340,6 @@ public AccountMigration createAccountForMigrationFuturePendingChange() { return createAccountForMigrationTest(input); } - public SubscriptionBaseTimeline createSubscriptionRepair(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 1; - } - }; - } - - public 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; - } - }; - } - - public 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); - return 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; - } - }; - } - - public 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 SubscriptionRepair " + id); - return null; - } - - public void validateExistingEventForAssertion(final ExistingEvent expected, final ExistingEvent input) { - log.debug(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getProductName(), expected.getPlanPhaseSpecifier().getProductName())); - assertEquals(input.getPlanPhaseSpecifier().getProductName(), expected.getPlanPhaseSpecifier().getProductName()); - log.debug(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getPhaseType(), expected.getPlanPhaseSpecifier().getPhaseType())); - assertEquals(input.getPlanPhaseSpecifier().getPhaseType(), expected.getPlanPhaseSpecifier().getPhaseType()); - log.debug(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getProductCategory(), expected.getPlanPhaseSpecifier().getProductCategory())); - assertEquals(input.getPlanPhaseSpecifier().getProductCategory(), expected.getPlanPhaseSpecifier().getProductCategory()); - log.debug(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getPriceListName(), expected.getPlanPhaseSpecifier().getPriceListName())); - assertEquals(input.getPlanPhaseSpecifier().getPriceListName(), expected.getPlanPhaseSpecifier().getPriceListName()); - log.debug(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getBillingPeriod(), expected.getPlanPhaseSpecifier().getBillingPeriod())); - assertEquals(input.getPlanPhaseSpecifier().getBillingPeriod(), expected.getPlanPhaseSpecifier().getBillingPeriod()); - log.debug(String.format("Got %s -> Expected %s", input.getEffectiveDate(), expected.getEffectiveDate())); - assertEquals(input.getEffectiveDate(), expected.getEffectiveDate()); - } - - public DeletedEvent createDeletedEvent(final UUID eventId) { - return new DeletedEvent() { - @Override - public UUID getEventId() { - return eventId; - } - }; - } - - public 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; - } - }; - } - - public 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()); - } - } - } - - public 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()); - } - }); - } - - public 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()); - } - }); - } - public static DateTime addOrRemoveDuration(final DateTime input, final List durations, final boolean add) { DateTime result = input; for (final Duration cur : durations) { @@ -628,22 +367,12 @@ public static DateTime addDuration(final DateTime input, final List du return addOrRemoveDuration(input, durations, true); } - public static DateTime removeDuration(final DateTime input, final List durations) { - return addOrRemoveDuration(input, durations, false); - } - public static DateTime addDuration(final DateTime input, final Duration duration) { final List list = new ArrayList(); list.add(duration); return addOrRemoveDuration(input, list, true); } - public static DateTime removeDuration(final DateTime input, final Duration duration) { - final List list = new ArrayList(); - list.add(duration); - return addOrRemoveDuration(input, list, false); - } - public static class SubscriptionMigrationCaseWithCTD implements SubscriptionMigrationCase { private final PlanPhaseSpecifier pps; @@ -678,20 +407,4 @@ public DateTime getChargedThroughDate() { } } - public interface TestWithExceptionCallback { - - public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException; - } - - public static class TestWithException { - - public void withException(final TestWithExceptionCallback callback, final ErrorCode code) throws Exception { - try { - callback.doTest(); - Assert.fail("Failed to catch exception " + code); - } catch (SubscriptionBaseRepairException e) { - assertEquals(e.getCode(), code.getCode()); - } - } - } } diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java index 1053238e96..a3acb5000c 100644 --- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java +++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java @@ -233,4 +233,29 @@ public void testUncancel() throws SubscriptionBillingApiException, SubscriptionB assertListenerStatus(); } + + @Test(groups = "slow", expectedExceptions = SubscriptionBaseApiException.class) + public void testCancelSubscriptionWithInvalidRequestedDate() throws SubscriptionBaseApiException { + final String prod = "Shotgun"; + final BillingPeriod term = BillingPeriod.MONTHLY; + final String planSet = PriceListSet.DEFAULT_PRICELIST_NAME; + + // CREATE + final DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, prod, term, planSet); + PlanPhase currentPhase = subscription.getCurrentPhase(); + assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL); + + // MOVE TO NEXT PHASE + testListener.pushExpectedEvent(NextEvent.PHASE); + + final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31)); + clock.addDeltaFromReality(it.toDurationMillis()); + assertListenerStatus(); + currentPhase = subscription.getCurrentPhase(); + assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN); + + final DateTime invalidDate = subscription.getBundleStartDate().minusDays(3); + // CANCEL in EVERGREEN period with an invalid Date (prior to the Creation Date) + subscription.cancelWithDate(invalidDate, callContext); + } } diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiChangePlan.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiChangePlan.java index 35f6bed749..f3418f9eed 100644 --- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiChangePlan.java +++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiChangePlan.java @@ -21,6 +21,8 @@ import org.joda.time.DateTime; import org.joda.time.Interval; +import org.killbill.billing.ErrorCode; +import org.killbill.billing.catalog.api.PlanAlignmentCreate; import org.testng.Assert; import org.testng.annotations.Test; @@ -427,4 +429,32 @@ public void testCorrectPhaseAlignmentOnChange() throws SubscriptionBaseApiExcept assertListenerStatus(); } + + @Test(groups = "slow") + public void testInvalidChangesAcrossProductTypes() throws SubscriptionBaseApiException { + final String baseProduct = "Shotgun"; + final BillingPeriod baseTerm = BillingPeriod.MONTHLY; + final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME; + + // CREATE BP + final DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList); + + // MOVE CLOCK 14 DAYS LATER + Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(14)); + clock.addDeltaFromReality(it.toDurationMillis()); + + // Create AO + final String aoProduct = "Laser-Scope"; + final BillingPeriod aoTerm = BillingPeriod.MONTHLY; + final String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME; + DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, aoProduct, aoTerm, aoPriceList); + + try { + aoSubscription.changePlanWithDate(baseProduct, baseTerm, basePriceList, null, clock.getUTCNow(), callContext); + Assert.fail("Should not allow plan change across product type"); + } catch (final SubscriptionBaseApiException e) { + Assert.assertEquals(e.getCode(), ErrorCode.SUB_CHANGE_INVALID.getCode()); + } + } + } diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java index 1891725161..1d95133a6f 100644 --- a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java +++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java @@ -41,7 +41,6 @@ import org.killbill.billing.subscription.api.migration.AccountMigrationData; import org.killbill.billing.subscription.api.migration.AccountMigrationData.BundleMigrationData; import org.killbill.billing.subscription.api.migration.AccountMigrationData.SubscriptionMigrationData; -import org.killbill.billing.subscription.api.timeline.SubscriptionDataRepair; import org.killbill.billing.subscription.api.transfer.TransferCancelData; import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase; import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle; @@ -376,7 +375,7 @@ private void cancelNextPhaseEvent(final UUID subscriptionId, final InternalTenan } if (cur.getType() == EventType.PHASE && cur.getEffectiveDate().isAfter(clock.getUTCNow())) { - cur.deactivate(); + it.remove(); break; } @@ -395,9 +394,9 @@ private void cancelNextChangeEvent(final UUID subscriptionId) { continue; } if (cur.getType() == EventType.API_USER && - ApiEventType.CHANGE == ((ApiEvent) cur).getEventType() && + ApiEventType.CHANGE == ((ApiEvent) cur).getApiEventType() && cur.getEffectiveDate().isAfter(clock.getUTCNow())) { - cur.deactivate(); + it.remove(); break; } } @@ -417,8 +416,8 @@ public void uncancelSubscription(final DefaultSubscriptionBase subscription, fin continue; } if (cur.getType() == EventType.API_USER && - ((ApiEvent) cur).getEventType() == ApiEventType.CANCEL) { - cur.deactivate(); + ((ApiEvent) cur).getApiEventType() == ApiEventType.CANCEL) { + it.remove(); foundCancel = true; break; } @@ -485,11 +484,6 @@ public Iterable getFutureEventsForAccount(final InternalT return null; } - @Override - public void repair(final UUID accountId, final UUID bundleId, final List inRepair, - final InternalCallContext context) { - } - @Override public void transfer(final UUID srcAccountId, final UUID destAccountId, final BundleMigrationData data, final List transferCancelData, final InternalCallContext fromContext, diff --git a/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleNoDB.java b/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleNoDB.java index ae1d4b7207..1bb0955e34 100644 --- a/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleNoDB.java +++ b/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleNoDB.java @@ -22,14 +22,10 @@ import org.killbill.billing.account.api.AccountUserApi; import org.killbill.billing.mock.glue.MockNonEntityDaoModule; import org.killbill.billing.platform.api.KillbillConfigSource; -import org.killbill.billing.subscription.api.timeline.RepairSubscriptionLifecycleDao; import org.killbill.billing.subscription.engine.dao.MockSubscriptionDaoMemory; -import org.killbill.billing.subscription.engine.dao.RepairSubscriptionDao; import org.killbill.billing.subscription.engine.dao.SubscriptionDao; import org.mockito.Mockito; -import com.google.inject.name.Names; - public class TestDefaultSubscriptionModuleNoDB extends TestDefaultSubscriptionModule { public TestDefaultSubscriptionModuleNoDB(final KillbillConfigSource configSource) { @@ -39,9 +35,6 @@ public TestDefaultSubscriptionModuleNoDB(final KillbillConfigSource configSource @Override protected void installSubscriptionDao() { bind(SubscriptionDao.class).to(MockSubscriptionDaoMemory.class).asEagerSingleton(); - bind(SubscriptionDao.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionDao.class); - bind(RepairSubscriptionLifecycleDao.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionDao.class); - bind(RepairSubscriptionDao.class).asEagerSingleton(); } @Override diff --git a/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleWithEmbeddedDB.java b/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleWithEmbeddedDB.java index 6d8cfa2eb3..7918876bf6 100644 --- a/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleWithEmbeddedDB.java +++ b/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleWithEmbeddedDB.java @@ -21,15 +21,11 @@ import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule; import org.killbill.billing.account.glue.DefaultAccountModule; import org.killbill.billing.platform.api.KillbillConfigSource; -import org.killbill.billing.subscription.api.timeline.RepairSubscriptionLifecycleDao; import org.killbill.billing.subscription.engine.dao.MockSubscriptionDaoSql; -import org.killbill.billing.subscription.engine.dao.RepairSubscriptionDao; import org.killbill.billing.subscription.engine.dao.SubscriptionDao; import org.killbill.billing.util.glue.CustomFieldModule; import org.killbill.billing.util.glue.NonEntityDaoModule; -import com.google.inject.name.Names; - public class TestDefaultSubscriptionModuleWithEmbeddedDB extends TestDefaultSubscriptionModule { public TestDefaultSubscriptionModuleWithEmbeddedDB(final KillbillConfigSource configSource) { @@ -39,9 +35,6 @@ public TestDefaultSubscriptionModuleWithEmbeddedDB(final KillbillConfigSource co @Override protected void installSubscriptionDao() { bind(SubscriptionDao.class).to(MockSubscriptionDaoSql.class).asEagerSingleton(); - bind(SubscriptionDao.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionDao.class); - bind(RepairSubscriptionLifecycleDao.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionDao.class); - bind(RepairSubscriptionDao.class).asEagerSingleton(); } @Override diff --git a/tenant/pom.xml b/tenant/pom.xml index aef172bdea..516ef5a815 100644 --- a/tenant/pom.xml +++ b/tenant/pom.xml @@ -19,7 +19,7 @@ killbill org.kill-bill.billing - 0.15.3-SNAPSHOT + 0.15.7-SNAPSHOT ../pom.xml killbill-tenant diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java b/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java index 776e6b1267..6b3a2bbb9f 100644 --- a/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java +++ b/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java @@ -42,6 +42,7 @@ import org.killbill.billing.util.config.TenantConfig; import org.killbill.bus.api.PersistentBus; import org.killbill.bus.api.PersistentBus.EventBusException; +import org.killbill.commons.concurrent.Executors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,22 +68,21 @@ public class TenantCacheInvalidation { private final Map cache; private final TenantBroadcastDao broadcastDao; - private final ScheduledExecutorService tenantExecutor; private final TenantConfig tenantConfig; private final PersistentBus eventBus; private final TenantDao tenantDao; private AtomicLong latestRecordIdProcessed; private volatile boolean isStopped; + private ScheduledExecutorService tenantExecutor; + @Inject public TenantCacheInvalidation(@Named(DefaultTenantModule.NO_CACHING_TENANT) final TenantBroadcastDao broadcastDao, - @Named(DefaultTenantModule.TENANT_EXECUTOR_NAMED) final ScheduledExecutorService tenantExecutor, @Named(DefaultTenantModule.NO_CACHING_TENANT) final TenantDao tenantDao, final PersistentBus eventBus, final TenantConfig tenantConfig) { this.cache = new HashMap(); this.broadcastDao = broadcastDao; - this.tenantExecutor = tenantExecutor; this.tenantConfig = tenantConfig; this.tenantDao = tenantDao; this.eventBus = eventBus; @@ -92,14 +92,11 @@ public TenantCacheInvalidation(@Named(DefaultTenantModule.NO_CACHING_TENANT) fin public void initialize() { final TenantBroadcastModelDao entry = broadcastDao.getLatestEntry(); this.latestRecordIdProcessed = entry != null ? new AtomicLong(entry.getRecordId()) : new AtomicLong(0L); - + this.tenantExecutor = Executors.newSingleThreadScheduledExecutor("TenantExecutor"); + this.isStopped = false; } public void start() { - if (isStopped) { - logger.warn("TenantExecutor is in a stopped state, abort start sequence"); - return; - } final TimeUnit pendingRateUnit = tenantConfig.getTenantBroadcastServiceRunningRate().getUnit(); final long pendingPeriod = tenantConfig.getTenantBroadcastServiceRunningRate().getPeriod(); tenantExecutor.scheduleAtFixedRate(new TenantCacheInvalidationRunnable(this, broadcastDao, tenantDao), pendingPeriod, pendingPeriod, pendingRateUnit); diff --git a/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java b/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java index 14ada6f26d..4d929d4519 100644 --- a/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java +++ b/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.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: * @@ -35,6 +37,7 @@ import org.killbill.billing.tenant.api.TenantKV.TenantKey; import org.killbill.billing.util.UUIDs; import org.killbill.billing.util.cache.CacheControllerDispatcher; +import org.killbill.billing.util.config.SecurityConfig; import org.killbill.billing.util.dao.NonEntityDao; import org.killbill.billing.util.entity.dao.EntityDaoBase; import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper; @@ -56,9 +59,12 @@ public class DefaultTenantDao extends EntityDaoBase() { @Override diff --git a/tenant/src/main/java/org/killbill/billing/tenant/glue/DefaultTenantModule.java b/tenant/src/main/java/org/killbill/billing/tenant/glue/DefaultTenantModule.java index 6770c67440..af1c9f7afe 100644 --- a/tenant/src/main/java/org/killbill/billing/tenant/glue/DefaultTenantModule.java +++ b/tenant/src/main/java/org/killbill/billing/tenant/glue/DefaultTenantModule.java @@ -18,8 +18,6 @@ package org.killbill.billing.tenant.glue; -import java.util.concurrent.ScheduledExecutorService; - import org.killbill.billing.glue.TenantModule; import org.killbill.billing.platform.api.KillbillConfigSource; import org.killbill.billing.tenant.api.DefaultTenantInternalApi; @@ -46,8 +44,6 @@ public class DefaultTenantModule extends KillBillModule implements TenantModule public static final String NO_CACHING_TENANT = "NoCachingTenant"; - public static final String TENANT_EXECUTOR_NAMED = "TenantExecutor"; - public DefaultTenantModule(final KillbillConfigSource configSource) { super(configSource); } @@ -79,11 +75,6 @@ public void installTenantCacheInvalidation() { bind(TenantCacheInvalidation.class).asEagerSingleton(); } - protected void installExecutor() { - final ScheduledExecutorService tenantExecutor = org.killbill.commons.concurrent.Executors.newSingleThreadScheduledExecutor("TenantExecutor"); - bind(ScheduledExecutorService.class).annotatedWith(Names.named(TENANT_EXECUTOR_NAMED)).toInstance(tenantExecutor); - } - @Override protected void configure() { installConfig(); @@ -91,6 +82,5 @@ protected void configure() { installTenantService(); installTenantUserApi(); installTenantCacheInvalidation(); - installExecutor(); } } diff --git a/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteWithEmbeddedDb.java b/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteWithEmbeddedDb.java index 48b6287bb7..2c4f78f5ba 100644 --- a/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteWithEmbeddedDb.java +++ b/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteWithEmbeddedDb.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: * @@ -18,18 +20,17 @@ import javax.inject.Named; +import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB; import org.killbill.billing.tenant.api.TenantUserApi; -import org.killbill.billing.tenant.dao.NoCachingTenantBroadcastDao; +import org.killbill.billing.tenant.dao.DefaultTenantDao; import org.killbill.billing.tenant.dao.TenantBroadcastDao; import org.killbill.billing.tenant.glue.DefaultTenantModule; +import org.killbill.billing.tenant.glue.TestTenantModuleWithEmbeddedDB; +import org.killbill.billing.util.config.SecurityConfig; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; -import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB; -import org.killbill.billing.tenant.dao.DefaultTenantDao; -import org.killbill.billing.tenant.glue.TestTenantModuleWithEmbeddedDB; - import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; @@ -49,6 +50,9 @@ public class TenantTestSuiteWithEmbeddedDb extends GuicyKillbillTestSuiteWithEmb @Inject protected TenantBroadcastDao tenantBroadcastDao; + @Inject + protected SecurityConfig securityConfig; + @BeforeClass(groups = "slow") protected void beforeClass() throws Exception { final Injector injector = Guice.createInjector(new TestTenantModuleWithEmbeddedDB(configSource)); diff --git a/tenant/src/test/java/org/killbill/billing/tenant/dao/TestDefaultTenantDao.java b/tenant/src/test/java/org/killbill/billing/tenant/dao/TestDefaultTenantDao.java index d23e86ac73..112e3ba1d5 100644 --- a/tenant/src/test/java/org/killbill/billing/tenant/dao/TestDefaultTenantDao.java +++ b/tenant/src/test/java/org/killbill/billing/tenant/dao/TestDefaultTenantDao.java @@ -45,11 +45,11 @@ public void testWeCanStoreAndMatchCredentials() throws Exception { // Good combo final AuthenticationToken goodToken = new UsernamePasswordToken(tenant.getApiKey(), tenant.getApiSecret()); - Assert.assertTrue(KillbillCredentialsMatcher.getCredentialsMatcher().doCredentialsMatch(goodToken, authenticationInfo)); + Assert.assertTrue(KillbillCredentialsMatcher.getCredentialsMatcher(securityConfig).doCredentialsMatch(goodToken, authenticationInfo)); // Bad combo final AuthenticationToken badToken = new UsernamePasswordToken(tenant.getApiKey(), tenant.getApiSecret() + "T"); - Assert.assertFalse(KillbillCredentialsMatcher.getCredentialsMatcher().doCredentialsMatch(badToken, authenticationInfo)); + Assert.assertFalse(KillbillCredentialsMatcher.getCredentialsMatcher(securityConfig).doCredentialsMatch(badToken, authenticationInfo)); } @Test(groups = "slow") diff --git a/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.java b/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.java index c2acf956fb..0bae0d124f 100644 --- a/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.java +++ b/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.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 @@ -21,6 +21,8 @@ import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule; import org.killbill.billing.platform.api.KillbillConfigSource; import org.killbill.billing.util.glue.NonEntityDaoModule; +import org.killbill.billing.util.glue.SecurityModule; +import org.killbill.billing.util.glue.TestUtilModuleNoDB.ShiroModuleNoDB; public class TestTenantModuleWithEmbeddedDB extends TestTenantModule { @@ -34,5 +36,7 @@ public void configure() { install(new GuicyKillbillTestWithEmbeddedDBModule(configSource)); install(new NonEntityDaoModule(configSource)); + install(new SecurityModule(configSource)); + install(new ShiroModuleNoDB(configSource)); } } diff --git a/usage/pom.xml b/usage/pom.xml index 4cd57d89d3..a08114a7af 100644 --- a/usage/pom.xml +++ b/usage/pom.xml @@ -19,7 +19,7 @@ killbill org.kill-bill.billing - 0.15.3-SNAPSHOT + 0.15.7-SNAPSHOT ../pom.xml killbill-usage diff --git a/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModuleWithEmbeddedDB.java b/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModuleWithEmbeddedDB.java index 4db4fd8b51..1e71a186b0 100644 --- a/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModuleWithEmbeddedDB.java +++ b/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModuleWithEmbeddedDB.java @@ -20,6 +20,8 @@ import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule; import org.killbill.billing.platform.api.KillbillConfigSource; +import org.killbill.billing.util.glue.CacheModule; +import org.killbill.billing.util.glue.NonEntityDaoModule; public class TestUsageModuleWithEmbeddedDB extends TestUsageModule { @@ -32,5 +34,7 @@ public void configure() { super.configure(); install(new GuicyKillbillTestWithEmbeddedDBModule(configSource)); + install(new CacheModule(configSource)); + install(new NonEntityDaoModule(configSource)); } } diff --git a/util/pom.xml b/util/pom.xml index 0a88a19191..961a2559be 100644 --- a/util/pom.xml +++ b/util/pom.xml @@ -21,7 +21,7 @@ killbill org.kill-bill.billing - 0.15.3-SNAPSHOT + 0.15.7-SNAPSHOT ../pom.xml killbill-util diff --git a/util/src/main/java/org/killbill/billing/util/cache/AccountBCDCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/AccountBCDCacheLoader.java new file mode 100644 index 0000000000..d88ea696d5 --- /dev/null +++ b/util/src/main/java/org/killbill/billing/util/cache/AccountBCDCacheLoader.java @@ -0,0 +1,59 @@ +/* + * Copyright 2014-2015 Groupon, Inc + * Copyright 2014-2015 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.killbill.billing.util.cache; + +import java.util.UUID; + +import org.killbill.billing.callcontext.InternalTenantContext; +import org.killbill.billing.util.cache.Cachable.CacheType; + +public class AccountBCDCacheLoader extends BaseCacheLoader { + + @Override + public CacheType getCacheType() { + return CacheType.ACCOUNT_BCD; + } + + @Override + public Object load(final Object key, final Object argument) { + + checkCacheLoaderStatus(); + + if (!(key instanceof UUID)) { + throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName()); + } + + if (!(argument instanceof CacheLoaderArgument)) { + throw new IllegalArgumentException("Unexpected argument type of " + argument.getClass().getName()); + } + + final CacheLoaderArgument cacheLoaderArgument = (CacheLoaderArgument) argument; + + if (cacheLoaderArgument.getArgs() == null || + !(cacheLoaderArgument.getArgs()[0] instanceof LoaderCallback)) { + throw new IllegalArgumentException("Missing LoaderCallback from the arguments "); + } + + final LoaderCallback callback = (LoaderCallback) cacheLoaderArgument.getArgs()[0]; + return callback.loadAccountBCD((UUID) key, cacheLoaderArgument.getInternalTenantContext()); + } + + public interface LoaderCallback { + Object loadAccountBCD(final UUID accountId, final InternalTenantContext context); + } +} diff --git a/util/src/main/java/org/killbill/billing/util/cache/Cachable.java b/util/src/main/java/org/killbill/billing/util/cache/Cachable.java index 767cc94cb7..bfc0b2718b 100644 --- a/util/src/main/java/org/killbill/billing/util/cache/Cachable.java +++ b/util/src/main/java/org/killbill/billing/util/cache/Cachable.java @@ -25,20 +25,22 @@ @Target({ElementType.METHOD}) public @interface Cachable { - public final String RECORD_ID_CACHE_NAME = "record-id"; - public final String ACCOUNT_RECORD_ID_CACHE_NAME = "account-record-id"; - public final String TENANT_RECORD_ID_CACHE_NAME = "tenant-record-id"; - public final String OBJECT_ID_CACHE_NAME = "object-id"; - public final String AUDIT_LOG_CACHE_NAME = "audit-log"; - public final String AUDIT_LOG_VIA_HISTORY_CACHE_NAME = "audit-log-via-history"; - public final String TENANT_CATALOG_CACHE_NAME = "tenant-catalog"; - public final String TENANT_OVERDUE_CONFIG_CACHE_NAME = "tenant-overdue-config"; - public final String TENANT_KV_CACHE_NAME = "tenant-kv"; - public final String OVERRIDDEN_PLAN_CACHE_NAME = "overridden-plan"; - - public CacheType value(); - - public enum CacheType { + String RECORD_ID_CACHE_NAME = "record-id"; + String ACCOUNT_RECORD_ID_CACHE_NAME = "account-record-id"; + String TENANT_RECORD_ID_CACHE_NAME = "tenant-record-id"; + String OBJECT_ID_CACHE_NAME = "object-id"; + String AUDIT_LOG_CACHE_NAME = "audit-log"; + String AUDIT_LOG_VIA_HISTORY_CACHE_NAME = "audit-log-via-history"; + String TENANT_CATALOG_CACHE_NAME = "tenant-catalog"; + String TENANT_OVERDUE_CONFIG_CACHE_NAME = "tenant-overdue-config"; + String TENANT_KV_CACHE_NAME = "tenant-kv"; + String OVERRIDDEN_PLAN_CACHE_NAME = "overridden-plan"; + String ACCOUNT_IMMUTABLE_CACHE_NAME = "account-immutable"; + String ACCOUNT_BCD_CACHE_NAME = "account-bcd"; + + CacheType value(); + + enum CacheType { /* Mapping from object 'id (UUID)' -> object 'recordId (Long' */ RECORD_ID(RECORD_ID_CACHE_NAME, false), @@ -68,7 +70,13 @@ public enum CacheType { TENANT_KV(TENANT_KV_CACHE_NAME, false), /* Overwritten plans */ - OVERRIDDEN_PLAN(OVERRIDDEN_PLAN_CACHE_NAME, false); + OVERRIDDEN_PLAN(OVERRIDDEN_PLAN_CACHE_NAME, false), + + /* Immutable account data config cache */ + ACCOUNT_IMMUTABLE(ACCOUNT_IMMUTABLE_CACHE_NAME, false), + + /* Account BCD config cache */ + ACCOUNT_BCD(ACCOUNT_BCD_CACHE_NAME, false); private final String cacheName; private final boolean isKeyPrefixedWithTableName; diff --git a/util/src/main/java/org/killbill/billing/util/cache/CacheController.java b/util/src/main/java/org/killbill/billing/util/cache/CacheController.java index 5743f65ca6..2c6be1ed71 100644 --- a/util/src/main/java/org/killbill/billing/util/cache/CacheController.java +++ b/util/src/main/java/org/killbill/billing/util/cache/CacheController.java @@ -20,13 +20,17 @@ public interface CacheController { - public void add(K key, V value); + void add(K key, V value); - public V get(K key, CacheLoaderArgument objectType); + V get(K key, CacheLoaderArgument objectType); - public boolean remove(K key); + V get(K key); - public int size(); + boolean remove(K key); + + void putIfAbsent(final K key, V value); + + int size(); void removeAll(); diff --git a/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java b/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java index 1f7998f469..08d04d203a 100644 --- a/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java +++ b/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java @@ -18,6 +18,8 @@ package org.killbill.billing.util.cache; +import javax.annotation.Nullable; + import org.killbill.billing.util.cache.Cachable.CacheType; import net.sf.ehcache.Ehcache; @@ -39,12 +41,18 @@ public void add(final K key, final V value) { } @Override - public V get(final K key, final CacheLoaderArgument cacheLoaderArgument) { - final Element element = cache.getWithLoader(key, null, cacheLoaderArgument); - if (element == null || element.getObjectValue() == null || element.getObjectValue().equals(BaseCacheLoader.EMPTY_VALUE_PLACEHOLDER)) { - return null; - } - return (V) element.getObjectValue(); + public V get(final K key, @Nullable final CacheLoaderArgument cacheLoaderArgument) { + return getWithOrWithoutCacheLoaderArgument(key, cacheLoaderArgument); + } + + @Override + public V get(final K key) { + return getWithOrWithoutCacheLoaderArgument(key, null); + } + + public void putIfAbsent(final K key, V value) { + final Element element = new Element(key, value); + cache.putIfAbsent(element); } @Override @@ -66,4 +74,13 @@ public void removeAll() { public CacheType getCacheType() { return cacheType; } + + private V getWithOrWithoutCacheLoaderArgument(final K key, @Nullable final CacheLoaderArgument cacheLoaderArgument) { + final Element element = cacheLoaderArgument != null ? cache.getWithLoader(key, null, cacheLoaderArgument) : cache.get(key); + if (element == null || element.getObjectValue() == null || element.getObjectValue().equals(BaseCacheLoader.EMPTY_VALUE_PLACEHOLDER)) { + return null; + } + return (V) element.getObjectValue(); + } + } diff --git a/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java b/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java index 42d89663ae..269ff66df5 100644 --- a/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java +++ b/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java @@ -51,6 +51,8 @@ public class EhCacheCacheManagerProvider implements Provider { @Inject public EhCacheCacheManagerProvider(final MetricRegistry metricRegistry, final CacheConfig cacheConfig, + final ImmutableAccountCacheLoader accountCacheLoader, + final AccountBCDCacheLoader accountBCDCacheLoader, final RecordIdCacheLoader recordIdCacheLoader, final AccountRecordIdCacheLoader accountRecordIdCacheLoader, final TenantRecordIdCacheLoader tenantRecordIdCacheLoader, @@ -63,6 +65,8 @@ public EhCacheCacheManagerProvider(final MetricRegistry metricRegistry, final OverriddenPlanCacheLoader overriddenPlanCacheLoader) { this.metricRegistry = metricRegistry; this.cacheConfig = cacheConfig; + cacheLoaders.add(accountCacheLoader); + cacheLoaders.add(accountBCDCacheLoader); cacheLoaders.add(recordIdCacheLoader); cacheLoaders.add(accountRecordIdCacheLoader); cacheLoaders.add(tenantRecordIdCacheLoader); diff --git a/util/src/main/java/org/killbill/billing/util/cache/ImmutableAccountCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/ImmutableAccountCacheLoader.java new file mode 100644 index 0000000000..866c44efd3 --- /dev/null +++ b/util/src/main/java/org/killbill/billing/util/cache/ImmutableAccountCacheLoader.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2015 Groupon, Inc + * Copyright 2014-2015 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.killbill.billing.util.cache; + +import org.killbill.billing.callcontext.InternalTenantContext; +import org.killbill.billing.util.cache.Cachable.CacheType; + +public class ImmutableAccountCacheLoader extends BaseCacheLoader { + + @Override + public CacheType getCacheType() { + return CacheType.ACCOUNT_IMMUTABLE; + } + + @Override + public Object load(final Object key, final Object argument) { + + checkCacheLoaderStatus(); + + if (!(key instanceof Long)) { + throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName()); + } + + if (!(argument instanceof CacheLoaderArgument)) { + throw new IllegalArgumentException("Unexpected argument type of " + argument.getClass().getName()); + } + + final CacheLoaderArgument cacheLoaderArgument = (CacheLoaderArgument) argument; + + if (cacheLoaderArgument.getArgs() == null || + !(cacheLoaderArgument.getArgs()[0] instanceof LoaderCallback)) { + throw new IllegalArgumentException("Missing LoaderCallback from the arguments "); + } + + final LoaderCallback callback = (LoaderCallback) cacheLoaderArgument.getArgs()[0]; + return callback.loadAccount((Long) key, cacheLoaderArgument.getInternalTenantContext()); + } + + public interface LoaderCallback { + Object loadAccount(final Long recordId, final InternalTenantContext context); + } +} diff --git a/util/src/main/java/org/killbill/billing/util/cache/TenantCatalogCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/TenantCatalogCacheLoader.java index 6a1accf450..897e75d4e9 100644 --- a/util/src/main/java/org/killbill/billing/util/cache/TenantCatalogCacheLoader.java +++ b/util/src/main/java/org/killbill/billing/util/cache/TenantCatalogCacheLoader.java @@ -81,7 +81,6 @@ public Object load(final Object key, final Object argument) { } public interface LoaderCallback { - public Object loadCatalog(final List catalogXMLs, final Long tenantRecordId) throws CatalogApiException; } } diff --git a/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java b/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java index f42a635fed..19b3368cb9 100644 --- a/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java +++ b/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java @@ -26,22 +26,27 @@ public interface InvoiceConfig extends KillbillConfig { @Config("org.killbill.invoice.maxNumberOfMonthsInFuture") @Default("36") @Description("Maximum target date to consider when generating an invoice") - public int getNumberOfMonthsInFuture(); + int getNumberOfMonthsInFuture(); @Config("org.killbill.invoice.emailNotificationsEnabled") @Default("false") @Description("Whether to send email notifications on invoice creation (for configured accounts)") - public boolean isEmailNotificationsEnabled(); + boolean isEmailNotificationsEnabled(); @Config("org.killbill.invoice.dryRunNotificationSchedule") @Default("0s") @Description("DryRun invoice notification time before targetDate (ignored if set to 0s)") - public TimeSpan getDryRunNotificationSchedule(); + TimeSpan getDryRunNotificationSchedule(); @Config("org.killbill.invoice.readMaxRawUsagePreviousPeriod") @Default("2") @Description("Maximum number of past billing periods we use to fetch raw usage data (usage optimization)") - public int getMaxRawUsagePreviousPeriod(); + int getMaxRawUsagePreviousPeriod(); + + @Config("org.killbill.invoice.globalLock.retries") + @Default("50") + @Description("Maximum number of times the system will retry to grab global lock (with a 100ms wait each time)") + int getMaxGlobalLockRetries(); } diff --git a/util/src/main/java/org/killbill/billing/util/config/JaxrsConfig.java b/util/src/main/java/org/killbill/billing/util/config/JaxrsConfig.java new file mode 100644 index 0000000000..a8583e8546 --- /dev/null +++ b/util/src/main/java/org/killbill/billing/util/config/JaxrsConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2015 Groupon, Inc + * Copyright 2014-2015 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.killbill.billing.util.config; + +import org.skife.config.Config; +import org.skife.config.Default; +import org.skife.config.Description; +import org.skife.config.TimeSpan; + +public interface JaxrsConfig extends KillbillConfig { + + @Config("org.killbill.jaxrs.threads.pool.nb") + @Default("30") + @Description("Number of threads for jaxrs executor") + int getJaxrsThreadNb(); + + @Config("org.killbill.jaxrs.timeout") + @Default("30s") + @Description("Total timeout for all callables associated to a given api call (parallel mode)") + TimeSpan getJaxrsTimeout(); + +} diff --git a/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java b/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java index 7b630c615e..9eadf6030b 100644 --- a/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java +++ b/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java @@ -29,60 +29,65 @@ public interface PaymentConfig extends KillbillConfig { // See ExternalPaymentProviderPlugin.PLUGIN_NAME @Default("__external_payment__") @Description("Default payment provider to use") - public String getDefaultPaymentProvider(); + String getDefaultPaymentProvider(); @Config("org.killbill.payment.retry.days") @Default("8,8,8") @Description("Specify the number of payment retries along with the interval in days between payment retries when payment failures occur") - public List getPaymentFailureRetryDays(); + List getPaymentFailureRetryDays(); @Config("org.killbill.payment.failure.retry.start.sec") @Default("300") @Description("Specify the interval of time in seconds before retrying a payment that failed due to a plugin failure (gateway is down, transient error, ...") - public int getPluginFailureInitialRetryInSec(); + int getPluginFailureInitialRetryInSec(); @Config("org.killbill.payment.failure.retry.multiplier") @Default("2") @Description("Specify the multiplier to apply between in retry before retrying a payment that failed due to a plugin failure (gateway is down, transient error, ...") - public int getPluginFailureRetryMultiplier(); + int getPluginFailureRetryMultiplier(); @Config("org.killbill.payment.failure.retry.max.attempts") @Default("8") @Description("Specify the max number of attempts before retrying a payment that failed due to a plugin failure (gateway is down, transient error, ...\"") - public int getPluginFailureRetryMaxAttempts(); + int getPluginFailureRetryMaxAttempts(); @Config("org.killbill.payment.plugin.timeout") @Default("30s") @Description("Timeout for each payment attempt") - public TimeSpan getPaymentPluginTimeout(); + TimeSpan getPaymentPluginTimeout(); @Config("org.killbill.payment.plugin.threads.nb") @Default("10") @Description("Number of threads for plugin executor dispatcher") - public int getPaymentPluginThreadNb(); + int getPaymentPluginThreadNb(); @Config("org.killbill.payment.janitor.attempts.delay") @Default("12h") @Description("Delay before which unresolved attempt should be retried") - public TimeSpan getIncompleteAttemptsTimeSpanDelay(); + TimeSpan getIncompleteAttemptsTimeSpanDelay(); @Config("org.killbill.payment.janitor.transactions.retries") @Default("15s,1m,3m,1h,1d,1d,1d,1d,1d") @Description("Delay before which unresolved transactions should be retried") - public List getIncompleteTransactionsRetries(); + List getIncompleteTransactionsRetries(); @Config("org.killbill.payment.janitor.rate") @Default("1h") @Description("Rate at which janitor tasks are scheduled") - public TimeSpan getJanitorRunningRate(); + TimeSpan getJanitorRunningRate(); @Config("org.killbill.payment.invoice.plugin") @Default("") @Description("Default payment control plugin names") - public List getPaymentControlPluginNames(); + List getPaymentControlPluginNames(); + + @Config("org.killbill.payment.globalLock.retries") + @Default("50") + @Description("Maximum number of times the system will retry to grab global lock (with a 100ms wait each time)") + int getMaxGlobalLockRetries(); @Config("org.killbill.payment.off") @Default("false") @Description("Whether the payment subsystem is off") - public boolean isPaymentOff(); + boolean isPaymentOff(); } diff --git a/util/src/main/java/org/killbill/billing/util/config/SecurityConfig.java b/util/src/main/java/org/killbill/billing/util/config/SecurityConfig.java index d53b6c602f..3b2382dcbe 100644 --- a/util/src/main/java/org/killbill/billing/util/config/SecurityConfig.java +++ b/util/src/main/java/org/killbill/billing/util/config/SecurityConfig.java @@ -28,6 +28,11 @@ public interface SecurityConfig extends KillbillConfig { @Description("Path to the shiro.ini file (classpath, url or file resource)") public String getShiroResourcePath(); + @Config("org.killbill.security.shiroNbHashIterations") + @Default("200000") + @Description("Sets the number of times submitted credentials will be hashed before comparing to the credentials stored in the system") + public Integer getShiroNbHashIterations(); + // LDAP Realm @Config("org.killbill.security.ldap.userDnTemplate") diff --git a/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldUserApi.java b/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldUserApi.java index ad05b91959..5abac2849b 100644 --- a/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldUserApi.java +++ b/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldUserApi.java @@ -119,7 +119,7 @@ public boolean apply(final CustomFieldModelDao input) { } for (CustomField cur : toBeInserted) { - customFieldDao.create(new CustomFieldModelDao(cur), internalCallContextFactory.createInternalCallContext(cur.getObjectId(), cur.getObjectType(), context)); + customFieldDao.create(new CustomFieldModelDao(context.getCreatedDate(), cur.getFieldName(), cur.getFieldValue(), cur.getObjectId(), cur.getObjectType()), internalCallContextFactory.createInternalCallContext(cur.getObjectId(), cur.getObjectType(), context)); } } diff --git a/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldModelDao.java b/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldModelDao.java index 7a96c922dd..9f4371be9b 100644 --- a/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldModelDao.java +++ b/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldModelDao.java @@ -19,11 +19,10 @@ import java.util.UUID; import org.joda.time.DateTime; - import org.killbill.billing.ObjectType; +import org.killbill.billing.util.UUIDs; import org.killbill.billing.util.customfield.CustomField; import org.killbill.billing.util.dao.TableName; -import org.killbill.billing.entity.EntityBase; import org.killbill.billing.util.entity.dao.EntityModelDao; import org.killbill.billing.util.entity.dao.EntityModelDaoBase; @@ -48,9 +47,8 @@ public CustomFieldModelDao(final UUID id, final DateTime createdDate, final Date this.isActive = true; } - public CustomFieldModelDao(final CustomField customField) { - this(customField.getId(), customField.getCreatedDate(), customField.getUpdatedDate(), customField.getFieldName(), - customField.getFieldValue(), customField.getObjectId(), customField.getObjectType()); + public CustomFieldModelDao(final DateTime createdDate, final String fieldName, final String fieldValue, final UUID objectId, final ObjectType objectType) { + this(UUIDs.randomUUID(), createdDate, createdDate, fieldName, fieldValue, objectId, objectType); } public String getFieldName() { diff --git a/util/src/main/java/org/killbill/billing/util/email/templates/MustacheTemplateEngine.java b/util/src/main/java/org/killbill/billing/util/email/templates/MustacheTemplateEngine.java index b9142207e7..a55c275ae0 100644 --- a/util/src/main/java/org/killbill/billing/util/email/templates/MustacheTemplateEngine.java +++ b/util/src/main/java/org/killbill/billing/util/email/templates/MustacheTemplateEngine.java @@ -25,7 +25,7 @@ public class MustacheTemplateEngine implements TemplateEngine { @Override public String executeTemplateText(final String templateText, final Map data) { - final Template template = Mustache.compiler().compile(templateText); + final Template template = Mustache.compiler().nullValue("").compile(templateText); return template.execute(data); } } diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/KillbillCredentialsMatcher.java b/util/src/main/java/org/killbill/billing/util/security/shiro/KillbillCredentialsMatcher.java index 7b5bc12cf8..1830226ef9 100644 --- a/util/src/main/java/org/killbill/billing/util/security/shiro/KillbillCredentialsMatcher.java +++ b/util/src/main/java/org/killbill/billing/util/security/shiro/KillbillCredentialsMatcher.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: * @@ -19,23 +21,21 @@ import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.crypto.hash.Sha512Hash; +import org.killbill.billing.util.config.SecurityConfig; public class KillbillCredentialsMatcher { - public static final String KILLBILL_TENANT_HASH_ITERATIONS_PROPERTY = "org.killbill.server.multitenant.hash_iterations"; - // See http://www.stormpath.com/blog/strong-password-hashing-apache-shiro and https://issues.apache.org/jira/browse/SHIRO-290 public static final String HASH_ALGORITHM_NAME = Sha512Hash.ALGORITHM_NAME; - public static final Integer HASH_ITERATIONS = Integer.parseInt(System.getProperty(KILLBILL_TENANT_HASH_ITERATIONS_PROPERTY, "200000")); private KillbillCredentialsMatcher() {} - public static CredentialsMatcher getCredentialsMatcher() { + public static CredentialsMatcher getCredentialsMatcher(final SecurityConfig securityConfig) { // This needs to be in sync with DefaultTenantDao final HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(HASH_ALGORITHM_NAME); // base64 encoding, not hex credentialsMatcher.setStoredCredentialsHexEncoded(false); - credentialsMatcher.setHashIterations(HASH_ITERATIONS); + credentialsMatcher.setHashIterations(securityConfig.getShiroNbHashIterations()); return credentialsMatcher; } diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/DefaultUserDao.java b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/DefaultUserDao.java index 92cc4f961a..28b9ca32cb 100644 --- a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/DefaultUserDao.java +++ b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/DefaultUserDao.java @@ -28,6 +28,7 @@ import org.joda.time.DateTime; import org.killbill.billing.ErrorCode; import org.killbill.billing.security.SecurityApiException; +import org.killbill.billing.util.config.SecurityConfig; import org.killbill.billing.util.security.shiro.KillbillCredentialsMatcher; import org.killbill.clock.Clock; import org.killbill.commons.jdbi.mapper.LowerToCamelBeanMapperFactory; @@ -43,17 +44,19 @@ public class DefaultUserDao implements UserDao { private static final RandomNumberGenerator rng = new SecureRandomNumberGenerator(); + private final IDBI dbi; private final Clock clock; + private final SecurityConfig securityConfig; @Inject - public DefaultUserDao(final IDBI dbi, final Clock clock) { + public DefaultUserDao(final IDBI dbi, final Clock clock, final SecurityConfig securityConfig) { this.dbi = dbi; this.clock = clock; + this.securityConfig = securityConfig; ((DBI) dbi).registerMapper(new LowerToCamelBeanMapperFactory(UserModelDao.class)); ((DBI) dbi).registerMapper(new LowerToCamelBeanMapperFactory(UserRolesModelDao.class)); ((DBI) dbi).registerMapper(new LowerToCamelBeanMapperFactory(RolesPermissionsModelDao.class)); - } @Override @@ -61,7 +64,7 @@ public void insertUser(final String username, final String password, final List< final ByteSource salt = rng.nextBytes(); final String hashedPasswordBase64 = new SimpleHash(KillbillCredentialsMatcher.HASH_ALGORITHM_NAME, - password, salt.toBase64(), KillbillCredentialsMatcher.HASH_ITERATIONS).toBase64(); + password, salt.toBase64(), securityConfig.getShiroNbHashIterations()).toBase64(); final DateTime createdDate = clock.getUTCNow(); dbi.inTransaction(new TransactionCallback() { @@ -136,7 +139,7 @@ public void updateUserPassword(final String username, final String password, fin final ByteSource salt = rng.nextBytes(); final String hashedPasswordBase64 = new SimpleHash(KillbillCredentialsMatcher.HASH_ALGORITHM_NAME, - password, salt.toBase64(), KillbillCredentialsMatcher.HASH_ITERATIONS).toBase64(); + password, salt.toBase64(), securityConfig.getShiroNbHashIterations()).toBase64(); dbi.inTransaction(new TransactionCallback() { @Override diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java index 9806abe4bb..380258ba24 100644 --- a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java +++ b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java @@ -24,6 +24,7 @@ import org.apache.shiro.realm.jdbc.JdbcRealm; import org.apache.shiro.subject.PrincipalCollection; import org.killbill.billing.platform.glue.KillBillPlatformModuleBase; +import org.killbill.billing.util.config.SecurityConfig; import org.killbill.billing.util.security.shiro.KillbillCredentialsMatcher; public class KillBillJdbcRealm extends JdbcRealm { @@ -33,11 +34,13 @@ public class KillBillJdbcRealm extends JdbcRealm { protected static final String KILLBILL_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ? and is_active"; private final DataSource dataSource; + private final SecurityConfig securityConfig; @Inject - public KillBillJdbcRealm(@Named(KillBillPlatformModuleBase.SHIRO_DATA_SOURCE_ID_NAMED) final DataSource dataSource) { + public KillBillJdbcRealm(@Named(KillBillPlatformModuleBase.SHIRO_DATA_SOURCE_ID_NAMED) final DataSource dataSource, final SecurityConfig securityConfig) { super(); this.dataSource = dataSource; + this.securityConfig = securityConfig; // Tweak JdbcRealm defaults setPermissionsLookupEnabled(true); @@ -56,7 +59,7 @@ public void clearCachedAuthorizationInfo(PrincipalCollection principals) { private void configureSecurity() { setSaltStyle(SaltStyle.COLUMN); - setCredentialsMatcher(KillbillCredentialsMatcher.getCredentialsMatcher()); + setCredentialsMatcher(KillbillCredentialsMatcher.getCredentialsMatcher(securityConfig)); } private void configureDataSource() { diff --git a/util/src/main/java/org/killbill/billing/util/tag/DefaultTagDefinition.java b/util/src/main/java/org/killbill/billing/util/tag/DefaultTagDefinition.java index 32d85f8edc..05ca6b49f3 100644 --- a/util/src/main/java/org/killbill/billing/util/tag/DefaultTagDefinition.java +++ b/util/src/main/java/org/killbill/billing/util/tag/DefaultTagDefinition.java @@ -44,13 +44,14 @@ public DefaultTagDefinition(final String name, final String description, final B } public DefaultTagDefinition(final UUID id, final String name, final String description, final Boolean isControlTag) { - this(id, name, description, isControlTag, ImmutableList.copyOf(ObjectType.values())); + this(id, name, description, isControlTag, getApplicableObjectTypes(id, isControlTag)); } public DefaultTagDefinition(final ControlTagType controlTag) { this(controlTag.getId(), controlTag.toString(), controlTag.getDescription(), true, controlTag.getApplicableObjectTypes()); } + @JsonCreator public DefaultTagDefinition(@JsonProperty("id") final UUID id, @JsonProperty("name") final String name, @@ -131,4 +132,16 @@ public int hashCode() { result = 31 * result + (applicableObjectTypes != null ? applicableObjectTypes.hashCode() : 0); return result; } + + private static List getApplicableObjectTypes(final UUID id, final Boolean isControlTag) { + if (!isControlTag) { + return ImmutableList.copyOf(ObjectType.values()); + } + for (final ControlTagType cur : ControlTagType.values()) { + if (cur.getId().equals(id)) { + return cur.getApplicableObjectTypes(); + } + } + throw new IllegalStateException(String.format("ControlTag id %s does not seem to exist", id)); + } } diff --git a/util/src/main/resources/ehcache.xml b/util/src/main/resources/ehcache.xml index 7769245f54..d172871328 100644 --- a/util/src/main/resources/ehcache.xml +++ b/util/src/main/resources/ehcache.xml @@ -168,6 +168,32 @@ properties=""/> + + + + + + + + diff --git a/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java index eac77a7438..565027ed29 100644 --- a/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java +++ b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java @@ -19,6 +19,7 @@ import javax.inject.Inject; import javax.sql.DataSource; +import org.killbill.billing.util.cache.CacheControllerDispatcher; import org.killbill.commons.embeddeddb.EmbeddedDB; import org.skife.jdbi.v2.IDBI; import org.slf4j.Logger; @@ -40,6 +41,10 @@ public class GuicyKillbillTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuite @Inject protected IDBI dbi; + @Inject + protected CacheControllerDispatcher controlCacheDispatcher; + + @BeforeSuite(groups = "slow") public void beforeSuite() throws Exception { DBTestingHelper.get().start(); @@ -51,6 +56,7 @@ public void beforeMethod() throws Exception { DBTestingHelper.get().getInstance().cleanupAllTables(); } catch (final Exception ignored) { } + controlCacheDispatcher.clearAll(); } @AfterSuite(groups = "slow") diff --git a/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java b/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java index caff3f8d54..6bdbbb32a6 100644 --- a/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java +++ b/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java @@ -26,6 +26,7 @@ import org.killbill.billing.util.audit.dao.AuditDao; import org.killbill.billing.util.cache.CacheControllerDispatcher; import org.killbill.billing.util.callcontext.InternalCallContextFactory; +import org.killbill.billing.util.config.SecurityConfig; import org.killbill.billing.util.customfield.api.DefaultCustomFieldUserApi; import org.killbill.billing.util.customfield.dao.CustomFieldDao; import org.killbill.billing.util.dao.NonEntityDao; @@ -59,8 +60,6 @@ public abstract class UtilTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuite @Inject protected PersistentBus eventBus; @Inject - protected CacheControllerDispatcher controlCacheDispatcher; - @Inject protected NonEntityDao nonEntityDao; @Inject protected InternalCallContextFactory internalCallContextFactory; @@ -86,6 +85,8 @@ public abstract class UtilTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuite protected TestApiListener eventsListener; @Inject protected SecurityApi securityApi; + @Inject + protected SecurityConfig securityConfig; @BeforeClass(groups = "slow") public void beforeClass() throws Exception { @@ -112,8 +113,6 @@ public void beforeMethod() throws Exception { eventBus.start(); eventBus.register(eventsListener); - controlCacheDispatcher.clearAll(); - // Make sure we start with a clean state assertListenerStatus(); } diff --git a/util/src/test/java/org/killbill/billing/util/customfield/TestFieldStore.java b/util/src/test/java/org/killbill/billing/util/customfield/TestFieldStore.java index 9051563f2a..9eed9861e2 100644 --- a/util/src/test/java/org/killbill/billing/util/customfield/TestFieldStore.java +++ b/util/src/test/java/org/killbill/billing/util/customfield/TestFieldStore.java @@ -1,7 +1,9 @@ /* * Copyright 2010-2011 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: * @@ -18,13 +20,12 @@ import java.util.UUID; -import org.testng.annotations.Test; - import org.killbill.billing.ObjectType; import org.killbill.billing.api.TestApiListener.NextEvent; import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB; import org.killbill.billing.util.api.CustomFieldApiException; import org.killbill.billing.util.customfield.dao.CustomFieldModelDao; +import org.testng.annotations.Test; public class TestFieldStore extends UtilTestSuiteWithEmbeddedDB { @@ -36,16 +37,14 @@ public void testCreateCustomField() throws CustomFieldApiException { String fieldName = "TestField1"; String fieldValue = "Kitty Hawk"; - final CustomField field = new StringCustomField(fieldName, fieldValue, objectType, id, internalCallContext.getCreatedDate()); eventsListener.pushExpectedEvent(NextEvent.CUSTOM_FIELD); - customFieldDao.create(new CustomFieldModelDao(field), internalCallContext); + customFieldDao.create(new CustomFieldModelDao(internalCallContext.getCreatedDate(), fieldName, fieldValue, id, objectType), internalCallContext); assertListenerStatus(); fieldName = "TestField2"; fieldValue = "Cape Canaveral"; - final CustomField field2 = new StringCustomField(fieldName, fieldValue, objectType, id, internalCallContext.getCreatedDate()); eventsListener.pushExpectedEvent(NextEvent.CUSTOM_FIELD); - customFieldDao.create(new CustomFieldModelDao(field2), internalCallContext); + customFieldDao.create(new CustomFieldModelDao(internalCallContext.getCreatedDate(), fieldName, fieldValue, id, objectType), internalCallContext); assertListenerStatus(); } } diff --git a/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java b/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java index 3be0174387..5a60177187 100644 --- a/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java +++ b/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java @@ -62,7 +62,10 @@ public Void withHandle(final Handle handle) throws Exception { // Verify the field was saved final List customFields = customFieldUserApi.getCustomFieldsForObject(accountId, ObjectType.ACCOUNT, callContext); Assert.assertEquals(customFields.size(), 1); - Assert.assertEquals(customFields.get(0), customField); + Assert.assertEquals(customFields.get(0).getFieldName(), customField.getFieldName()); + Assert.assertEquals(customFields.get(0).getFieldValue(), customField.getFieldValue()); + Assert.assertEquals(customFields.get(0).getObjectId(), customField.getObjectId()); + Assert.assertEquals(customFields.get(0).getObjectType(), customField.getObjectType()); // Verify the account_record_id was populated dbi.withHandle(new HandleCallback() { @Override diff --git a/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillJdbcRealm.java b/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillJdbcRealm.java index 540f375d32..931e546539 100644 --- a/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillJdbcRealm.java +++ b/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillJdbcRealm.java @@ -50,7 +50,7 @@ public class TestKillBillJdbcRealm extends UtilTestSuiteWithEmbeddedDB { @BeforeMethod(groups = "slow") public void beforeMethod() throws Exception { super.beforeMethod(); - final KillBillJdbcRealm realm = new KillBillJdbcRealm(helper.getDataSource()); + final KillBillJdbcRealm realm = new KillBillJdbcRealm(helper.getDataSource(), securityConfig); securityManager = new DefaultSecurityManager(realm); SecurityUtils.setSecurityManager(securityManager); } diff --git a/util/src/test/java/org/killbill/billing/util/tag/TestDefaultTagDefinition.java b/util/src/test/java/org/killbill/billing/util/tag/TestDefaultTagDefinition.java new file mode 100644 index 0000000000..d030b62eac --- /dev/null +++ b/util/src/test/java/org/killbill/billing/util/tag/TestDefaultTagDefinition.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2015 Groupon, Inc + * Copyright 2014-2015 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.killbill.billing.util.tag; + +import java.util.UUID; + +import org.killbill.billing.ObjectType; +import org.killbill.billing.util.UtilTestSuiteNoDB; +import org.killbill.billing.util.api.TagApiException; +import org.killbill.billing.util.api.TagDefinitionApiException; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableList; + +public class TestDefaultTagDefinition extends UtilTestSuiteNoDB { + + @Test(groups = "fast") + public void testDefaultTagDefinition() throws TagApiException, TagDefinitionApiException { + + final DefaultTagDefinition def1 = new DefaultTagDefinition(UUID.randomUUID(), "foo", "nothing", false); + Assert.assertFalse(def1.getApplicableObjectTypes().isEmpty()); + Assert.assertEquals(ImmutableList.copyOf(ObjectType.values()), def1.getApplicableObjectTypes()); + + for (final ControlTagType cur : ControlTagType.values()) { + + final DefaultTagDefinition curDef = new DefaultTagDefinition(cur.getId(), cur.name(), cur.getDescription(), true); + Assert.assertFalse(curDef.getApplicableObjectTypes().isEmpty()); + Assert.assertEquals(curDef.getApplicableObjectTypes(), cur.getApplicableObjectTypes()); + } + + try { + new DefaultTagDefinition(UUID.randomUUID(), "bar", "nothing again", true); + Assert.fail("Not a control tag type"); + } catch (final IllegalStateException e) { + } + } +}