killbill-aplcache
Changes
.gitignore 5(+5 -0)
.travis.yml 10(+1 -9)
account/src/main/resources/org/killbill/billing/account/migration/V20170915165117__external_key_not_null.sql 2(+2 -0)
api/src/main/java/org/killbill/billing/overdue/applicator/formatters/OverdueEmailFormatterFactory.java 24(+0 -24)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/BeatrixIntegrationModule.java 2(+0 -2)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java 3(+2 -1)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java 256(+256 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java 70(+65 -5)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java 25(+24 -1)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoInvoiceDraft.java 78(+77 -1)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java 2(+1 -1)
catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java 19(+1 -18)
catalog/src/main/resources/org/killbill/billing/catalog/migration/V20161220000000__unit_price_override.sql 15(+7 -8)
catalog/src/test/java/org/killbill/billing/catalog/plugin/TestModelVersionedPluginCatalog.java 8(+0 -8)
catalog/src/test/resources/catalogTest.xml 48(+47 -1)
entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java 2(+1 -1)
entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionApi.java 4(+2 -2)
entitlement/src/test/java/org/killbill/billing/entitlement/engine/core/TestEntitlementUtils.java 6(+3 -3)
invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java 17(+11 -6)
invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java 4(+2 -2)
invoice/src/main/java/org/killbill/billing/invoice/notification/EmailInvoiceNotifier.java 118(+0 -118)
invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java 10(+10 -0)
invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java 67(+25 -42)
invoice/src/test/java/org/killbill/billing/invoice/api/user/TestInvoiceFlagBehaviors.java 31(+0 -31)
invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java 84(+42 -42)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEventSet.java 11(+6 -5)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java 16(+14 -2)
NEWS 6(+6 -0)
overdue/src/main/java/org/killbill/billing/overdue/applicator/formatters/DefaultOverdueEmailFormatterFactory.java 27(+0 -27)
overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java 123(+31 -92)
overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java 2(+1 -1)
overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueCheckNotifier.java 2(+1 -1)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java 1(+1 -0)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java 58(+39 -19)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java 5(+5 -0)
payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java 2(+1 -1)
payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java 7(+6 -1)
payment/src/main/java/org/killbill/billing/payment/retry/DefaultPriorPaymentControlResult.java 14(+11 -3)
payment/src/test/java/org/killbill/billing/payment/api/TestPaymentGatewayApiWithPaymentControl.java 2(+1 -1)
payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java 4(+2 -2)
payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java 1(+1 -0)
payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentControlProviderPlugin.java 2(+1 -1)
profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/LoggingFilterObfuscator.java 57(+57 -0)
profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java 1(+1 -0)
profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java 3(+0 -3)
profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java 4(+4 -0)
profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestLoggingFilterObfuscator.java 81(+81 -0)
profiles/killpay/src/main/java/org/killbill/billing/server/modules/KillpayServerModule.java 2(+0 -2)
subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java 9(+5 -4)
subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java 50(+9 -41)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java 7(+4 -3)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java 21(+6 -15)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java 74(+71 -3)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java 3(+2 -1)
subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java 7(+3 -4)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java 8(+4 -4)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiChangePlan.java 35(+16 -19)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java 11(+11 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java 10(+5 -5)
subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java 2(+1 -1)
util/pom.xml 8(+4 -4)
util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java 95(+61 -34)
Details
.gitignore 5(+5 -0)
diff --git a/.gitignore b/.gitignore
index dea7666..251e19e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,6 +66,11 @@ modules.xml
.idea/vcs.xml
.idea/dbnavigator.xml
+# Eclipse metadata
+.classpath
+.project
+.settings/
+
#
# https://github.com/github/gitignore/blob/master/Java.gitignore
#
.travis.yml 10(+1 -9)
diff --git a/.travis.yml b/.travis.yml
index bdf121e..be5eefd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -34,19 +34,11 @@ matrix:
- env: PHASE="-Ptravis,jdk17" JDK=openjdk8
- env: PHASE="-Ptravis,jdk18" JDK=oraclejdk8
- env: PHASE="-Ptravis,jdk18" JDK=openjdk8
- - env: PHASE="-Pmysql,jdk16" JDK=oraclejdk8
- - env: PHASE="-Pmysql,jdk16" JDK=openjdk8
- - env: PHASE="-Pmysql,jdk17" JDK=oraclejdk8
- - env: PHASE="-Pmysql,jdk17" JDK=openjdk8
- env: PHASE="-Pmysql,jdk18" JDK=oraclejdk8
- env: PHASE="-Pmysql,jdk18" JDK=openjdk8
- - env: PHASE="-Ppostgresql,jdk16" JDK=oraclejdk8
- - env: PHASE="-Ppostgresql,jdk16" JDK=openjdk8
- - env: PHASE="-Ppostgresql,jdk17" JDK=oraclejdk8
- - env: PHASE="-Ppostgresql,jdk17" JDK=openjdk8
- env: PHASE="-Ppostgresql,jdk18" JDK=oraclejdk8
- env: PHASE="-Ppostgresql,jdk18" JDK=openjdk8
fast_finish: true
after_success:
- - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && echo "<settings><servers><server><id>sonatype-nexus-snapshots</id><username>\${env.OSSRH_USER}</username><password>\${env.OSSRH_PASS}</password></server></servers></settings>" > ~/settings.xml && mvn deploy -DskipTests=true --settings ~/settings.xml | egrep "WARN|ERR|\[INFO\]\ ---|Upload" | egrep -v "[0-9]+/[0-9]+ KB" ; rm -f ~/settings.xml'
+ - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && echo "<settings><servers><server><id>sonatype-nexus-snapshots</id><username>\${env.OSSRH_USER}</username><password>\${env.OSSRH_PASS}</password></server></servers></settings>" > ~/settings.xml && mvn deploy -DskipTests=true --settings ~/settings.xml | egrep "WARN|ERR|\[INFO\]\ ---|Upload" | egrep -v "[0-9]+/[0-9]+ [kKmM]B" ; rm -f ~/settings.xml'
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 195eb5b..5fa0931 100644
--- a/account/src/main/resources/org/killbill/billing/account/ddl.sql
+++ b/account/src/main/resources/org/killbill/billing/account/ddl.sql
@@ -4,7 +4,7 @@ DROP TABLE IF EXISTS accounts;
CREATE TABLE accounts (
record_id serial unique,
id varchar(36) NOT NULL,
- external_key varchar(255) NULL,
+ external_key varchar(255) NOT NULL,
email varchar(128) DEFAULT NULL,
name varchar(100) DEFAULT NULL,
first_name_length int DEFAULT NULL,
@@ -46,7 +46,7 @@ CREATE TABLE account_history (
record_id serial unique,
id varchar(36) NOT NULL,
target_record_id bigint /*! unsigned */ not null,
- external_key varchar(255) NULL,
+ external_key varchar(255) NOT NULL,
email varchar(128) DEFAULT NULL,
name varchar(100) DEFAULT NULL,
first_name_length int DEFAULT NULL,
diff --git a/account/src/main/resources/org/killbill/billing/account/migration/V20170915165117__external_key_not_null.sql b/account/src/main/resources/org/killbill/billing/account/migration/V20170915165117__external_key_not_null.sql
new file mode 100644
index 0000000..73b42fd
--- /dev/null
+++ b/account/src/main/resources/org/killbill/billing/account/migration/V20170915165117__external_key_not_null.sql
@@ -0,0 +1,2 @@
+alter table accounts modify external_key varchar(255) NOT NULL;
+alter table account_history modify external_key varchar(255) NOT NULL;
diff --git a/api/src/main/java/org/killbill/billing/junction/BillingEventSet.java b/api/src/main/java/org/killbill/billing/junction/BillingEventSet.java
index dcb4b2a..3f760c2 100644
--- a/api/src/main/java/org/killbill/billing/junction/BillingEventSet.java
+++ b/api/src/main/java/org/killbill/billing/junction/BillingEventSet.java
@@ -32,7 +32,7 @@ public interface BillingEventSet extends SortedSet<BillingEvent> {
public boolean isAccountAutoInvoiceDraft();
- public BillingMode getRecurringBillingMode();
+ public boolean isAccountAutoInvoiceReuseDraft();
public List<UUID> getSubscriptionIdsWithAutoInvoiceOff();
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
index b581b4c..b018613 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
@@ -27,6 +27,7 @@ import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.Product;
@@ -54,15 +55,15 @@ public interface SubscriptionBase extends Entity, Blockable {
throws SubscriptionBaseApiException;
// Return the effective date of the change
- public DateTime changePlan(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final CallContext context)
+ public DateTime changePlan(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final CallContext context)
throws SubscriptionBaseApiException;
// Return the effective date of the change
- public DateTime changePlanWithDate(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDate, final CallContext context)
+ public DateTime changePlanWithDate(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDate, final CallContext context)
throws SubscriptionBaseApiException;
// Return the effective date of the change
- public DateTime changePlanWithPolicy(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides,
+ public DateTime changePlanWithPolicy(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides,
final BillingActionPolicy policy, final CallContext context)
throws SubscriptionBaseApiException;
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
index 3bc9cb9..d17adb7 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
@@ -86,7 +86,7 @@ public interface SubscriptionBaseInternalApi {
public List<EffectiveSubscriptionInternalEvent> getBillingTransitions(SubscriptionBase subscription, InternalTenantContext context);
- public DateTime getDryRunChangePlanEffectiveDate(SubscriptionBase subscription, PlanSpecifier spec, DateTime requestedDate, BillingActionPolicy policy, List<PlanPhasePriceOverride> overrides, InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException;
+ public DateTime getDryRunChangePlanEffectiveDate(SubscriptionBase subscription, PlanPhaseSpecifier spec, DateTime requestedDate, BillingActionPolicy policy, List<PlanPhasePriceOverride> overrides, InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException;
public List<EntitlementAOStatusDryRun> getDryRunChangePlanStatus(UUID subscriptionId, @Nullable String baseProductName,
DateTime requestedDate, InternalTenantContext context) throws SubscriptionBaseApiException;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/BeatrixIntegrationModule.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/BeatrixIntegrationModule.java
index 5b501ad..0fae546 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/BeatrixIntegrationModule.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/BeatrixIntegrationModule.java
@@ -46,7 +46,6 @@ import org.killbill.billing.tenant.glue.DefaultTenantModule;
import org.killbill.billing.usage.glue.UsageModule;
import org.killbill.billing.util.config.definition.InvoiceConfig;
import org.killbill.billing.util.config.definition.PaymentConfig;
-import org.killbill.billing.util.email.EmailModule;
import org.killbill.billing.util.email.templates.TemplateModule;
import org.killbill.billing.util.glue.AuditModule;
import org.killbill.billing.util.glue.BroadcastModule;
@@ -88,7 +87,6 @@ public class BeatrixIntegrationModule extends KillBillModule {
install(new GlobalLockerModule(configSource));
install(new CacheModule(configSource));
install(new ConfigModule(configSource));
- install(new EmailModule(configSource));
install(new CallContextModule(configSource));
install(new TagStoreModule(configSource));
install(new CustomFieldModule(configSource));
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index 1851420..b4b9016 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -34,6 +34,7 @@ import org.killbill.billing.beatrix.integration.BeatrixIntegrationModule;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
@@ -1195,7 +1196,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
private void checkChangePlanWithOverdueState(final Entitlement entitlement, final boolean shouldFail, final boolean expectedPayment) {
if (shouldFail) {
try {
- entitlement.changePlan(new PlanSpecifier("Pistol", term, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), callContext);
+ entitlement.changePlan(new PlanPhaseSpecifier("Pistol", term, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), callContext);
} catch (EntitlementApiException e) {
assertTrue(e.getCause() instanceof BlockingApiException || e.getCode() == ErrorCode.SUB_CHANGE_NON_ACTIVE.getCode(),
String.format("Cause is %s, message is %s", e.getCause(), e.getMessage()));
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java
index f893c7e..33c8f44 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java
@@ -24,11 +24,13 @@ import org.killbill.billing.ErrorCode;
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;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.api.EntitlementApiException;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.payment.api.PluginProperty;
@@ -101,6 +103,260 @@ public class TestCatalogRetireElements extends TestIntegrationBase {
}
@Test(groups = "slow")
+ public void testRetirePlanWithUncancel() throws Exception {
+ // Catalog v1 starts in 2011-01-01
+ // Catalog v2 starts in 2015-12-01
+ final LocalDate today = new LocalDate(2015, 10, 5);
+
+ // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+ clock.setDay(today);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ final String productName = "Pistol";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+
+ Entitlement bpEntitlement =
+ createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName,
+ ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ assertNotNull(bpEntitlement);
+ assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 1);
+ assertEquals(bpEntitlement.getState(), EntitlementState.ACTIVE);
+
+ // Move out a month.
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // Cancel entitlement
+ bpEntitlement = bpEntitlement.cancelEntitlementWithDate(new LocalDate("2016-05-01"), true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(bpEntitlement.getState(), EntitlementState.ACTIVE);
+ assertListenerStatus();
+
+ // Move out a month.
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // Catalog v2 should start now.
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(productName, term, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ try {
+ entitlementApi.createBaseEntitlement(account.getId(), spec, "externalKey2", null, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ fail(); // force to fail is there is not an exception
+ } catch (final EntitlementApiException e) {
+ assertEquals(e.getCode(), ErrorCode.CAT_PLAN_NOT_FOUND.getCode());
+ }
+
+ // Uncancel entitlement
+ busHandler.pushExpectedEvents(NextEvent.UNCANCEL);
+ bpEntitlement.uncancelEntitlement(ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Move out a month and verify 'Pistol' plan continue working as expected.
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 4);
+ for (final Invoice invoice : invoices) {
+ assertEquals(invoice.getInvoiceItems().get(0).getPlanName(), "pistol-monthly");
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testRetirePlanAfterChange() throws Exception {
+ // Catalog v1 starts in 2011-01-01
+ // Catalog v3 starts in 2016-01-01
+ final LocalDate today = new LocalDate(2015, 7, 5);
+
+ // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+ clock.setDay(today);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
+
+ final String productName = "Pistol";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final PlanPhaseSpecifier spec1 = new PlanPhaseSpecifier(productName, term, "DEFAULT", null);
+
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ Entitlement bpEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec1, "externalKey", null, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ assertNotNull(bpEntitlement);
+ assertEquals(bpEntitlement.getLastActivePhase().getPhaseType(), PhaseType.TRIAL);
+ assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 1);
+
+ // Move out after trial (2015-08-04)
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ assertEquals(entitlementApi.getEntitlementForId(bpEntitlement.getId(), callContext).getLastActivePhase().getPhaseType(), PhaseType.EVERGREEN);
+ assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 2);
+
+ // 2015-08-05
+ clock.addDays(1);
+ assertListenerStatus();
+
+ // Change to discount phase in SpecialDiscount pricelist (CBA generated, no payment)
+ // Note that we need to trigger a CHANGE outside a TRIAL phase to generate a CHANGE event (otherwise, a CREATE is generated)
+ final PlanPhaseSpecifier spec2 = new PlanPhaseSpecifier(productName, term, "SpecialDiscount", null);
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE);
+ bpEntitlement = bpEntitlement.changePlanWithDate(spec2, null, clock.getUTCToday(), ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ assertEquals(entitlementApi.getEntitlementForId(bpEntitlement.getId(), callContext).getLastActivePhase().getPhaseType(), PhaseType.DISCOUNT);
+ assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 3);
+
+ // Move out after discount phase (happens on 2015-11-04)
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ // 2015-09-05
+ clock.addMonths(1);
+ assertListenerStatus();
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ // 2015-10-05
+ clock.addMonths(1);
+ assertListenerStatus();
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ // 2015-11-05
+ clock.addMonths(1);
+ // This verifies the PlanAligner.getNextTimedPhase codepath with a CHANGE transition type
+ assertListenerStatus();
+
+ assertEquals(entitlementApi.getEntitlementForId(bpEntitlement.getId(), callContext).getLastActivePhase().getPhaseType(), PhaseType.EVERGREEN);
+
+ // Move out a month (2015-12-05)
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // Move out a month (2016-01-01)
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // Catalog v3 should start now.
+
+ try {
+ entitlementApi.createBaseEntitlement(account.getId(), spec2, "externalKey2", null, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ fail(); // force to fail is there is not an exception
+ } catch (final EntitlementApiException e) {
+ assertEquals(e.getCode(), ErrorCode.CAT_NO_SUCH_PRODUCT.getCode());
+ }
+
+ // Move out a month and verify 'Pistol' discounted plan continues working as expected.
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 9);
+ }
+
+ @Test(groups = "slow")
+ public void testRetirePlanWithUncancelAfterChange() throws Exception {
+ // Catalog v1 starts in 2011-01-01
+ // Catalog v3 starts in 2016-01-01
+ final LocalDate today = new LocalDate(2015, 7, 5);
+
+ // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+ clock.setDay(today);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
+
+ final String productName = "Pistol";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final PlanPhaseSpecifier spec1 = new PlanPhaseSpecifier(productName, term, "DEFAULT", null);
+
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ Entitlement bpEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec1, "externalKey", null, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ assertNotNull(bpEntitlement);
+ assertEquals(bpEntitlement.getLastActivePhase().getPhaseType(), PhaseType.TRIAL);
+ assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 1);
+
+ // Move out after trial (2015-08-04)
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ assertEquals(entitlementApi.getEntitlementForId(bpEntitlement.getId(), callContext).getLastActivePhase().getPhaseType(), PhaseType.EVERGREEN);
+ assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 2);
+
+ // 2015-08-05
+ clock.addDays(1);
+ assertListenerStatus();
+
+ // Change to discount phase in SpecialDiscount pricelist (CBA generated, no payment)
+ // Note that we need to trigger a CHANGE outside a TRIAL phase to generate a CHANGE event (otherwise, a CREATE is generated)
+ final PlanPhaseSpecifier spec2 = new PlanPhaseSpecifier(productName, term, "SpecialDiscount", null);
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE);
+ bpEntitlement = bpEntitlement.changePlanWithDate(spec2, null, clock.getUTCToday(), ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ assertEquals(entitlementApi.getEntitlementForId(bpEntitlement.getId(), callContext).getLastActivePhase().getPhaseType(), PhaseType.DISCOUNT);
+ assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 3);
+
+ // Cancel entitlement
+ bpEntitlement = bpEntitlement.cancelEntitlementWithDate(new LocalDate("2016-05-01"), true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(bpEntitlement.getState(), EntitlementState.ACTIVE);
+ assertListenerStatus();
+
+ // Move out after discount phase (happens on 2015-11-04)
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ // 2015-09-05
+ clock.addMonths(1);
+ assertListenerStatus();
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ // 2015-10-05
+ clock.addMonths(1);
+ assertListenerStatus();
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ // 2015-11-05
+ clock.addMonths(1);
+ // This verifies the PlanAligner.getNextTimedPhase codepath with a CHANGE transition type
+ assertListenerStatus();
+
+ assertEquals(entitlementApi.getEntitlementForId(bpEntitlement.getId(), callContext).getLastActivePhase().getPhaseType(), PhaseType.EVERGREEN);
+
+ // Move out a month (2015-12-05)
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // Uncancel entitlement
+ busHandler.pushExpectedEvents(NextEvent.UNCANCEL);
+ bpEntitlement.uncancelEntitlement(ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Move out a month (2016-01-01)
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // Catalog v3 should start now.
+
+ try {
+ entitlementApi.createBaseEntitlement(account.getId(), spec2, "externalKey2", null, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ fail(); // force to fail is there is not an exception
+ } catch (final EntitlementApiException e) {
+ assertEquals(e.getCode(), ErrorCode.CAT_NO_SUCH_PRODUCT.getCode());
+ }
+
+ // Move out a month and verify 'Pistol' discounted plan continues working as expected.
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 9);
+ }
+
+ @Test(groups = "slow")
public void testRetireProduct() throws Exception {
// Catalog v1 starts in 2011-01-01
// Catalog v3 starts in 2016-01-01
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 d38cea6..1c7d63e 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
@@ -716,9 +716,9 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
// Need to fetch again to get latest CTD updated from the system
Entitlement refreshedEntitlement = entitlementApi.getEntitlementForId(entitlement.getId(), callContext);
if (billingPolicy == null) {
- refreshedEntitlement = refreshedEntitlement.changePlan(new PlanSpecifier(productName, billingPeriod, priceList), null, ImmutableList.<PluginProperty>of(), callContext);
+ refreshedEntitlement = refreshedEntitlement.changePlan(new PlanPhaseSpecifier(productName, billingPeriod, priceList), null, ImmutableList.<PluginProperty>of(), callContext);
} else {
- refreshedEntitlement = refreshedEntitlement.changePlanOverrideBillingPolicy(new PlanSpecifier(productName, billingPeriod, priceList), null, null, billingPolicy, ImmutableList.<PluginProperty>of(), callContext);
+ refreshedEntitlement = refreshedEntitlement.changePlanOverrideBillingPolicy(new PlanPhaseSpecifier(productName, billingPeriod, priceList), null, null, billingPolicy, ImmutableList.<PluginProperty>of(), callContext);
}
return refreshedEntitlement;
} catch (final EntitlementApiException e) {
@@ -810,12 +810,15 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
add_account_Tag(id, ControlTagType.AUTO_INVOICING_DRAFT, type);
}
+ protected void add_AUTO_INVOICING_REUSE_DRAFT_Tag(final UUID id, final ObjectType type) throws TagDefinitionApiException, TagApiException {
+ add_account_Tag(id, ControlTagType.AUTO_INVOICING_REUSE_DRAFT, type);
+ }
+
private void add_account_Tag(final UUID id, final ControlTagType controlTagType, final ObjectType type) throws TagDefinitionApiException, TagApiException {
busHandler.pushExpectedEvent(NextEvent.TAG);
tagUserApi.addTag(id, type, controlTagType.getId(), callContext);
assertListenerStatus();
- final List<Tag> tags = tagUserApi.getTagsForObject(id, type, false, callContext);
- assertEquals(tags.size(), 1);
+ tagUserApi.getTagsForObject(id, type, false, callContext);
}
protected void remove_AUTO_PAY_OFF_Tag(final UUID id, final ObjectType type, final NextEvent... additionalEvents) throws TagDefinitionApiException, TagApiException {
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 1f1028b..ae26efd 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
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
@@ -30,15 +30,16 @@ 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;
+import org.killbill.billing.catalog.DefaultPlanPhasePriceOverride;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
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.catalog.api.UsagePriceOverride;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.entitlement.api.Entitlement;
-import org.killbill.billing.entitlement.api.EntitlementApiException;
import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.DryRunType;
@@ -50,14 +51,12 @@ import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PluginProperty;
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 com.tc.util.Assert.fail;
import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
public class TestIntegrationInvoice extends TestIntegrationBase {
@@ -506,4 +505,65 @@ public class TestIntegrationInvoice extends TestIntegrationBase {
}
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/783")
+ public void testIntegrationWithRecurringFreePlan() throws Exception {
+ final DateTime initialCreationDate = new DateTime(2017, 1, 1, 0, 0, 0, 0, testTimeZone);
+ // set clock to the initial start date
+ clock.setTime(initialCreationDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "notrial", null);
+
+ // Price override of $0
+ final List<PlanPhasePriceOverride> overrides = new ArrayList<PlanPhasePriceOverride>();
+ overrides.add(new DefaultPlanPhasePriceOverride("blowdart-monthly-notrial-evergreen", account.getCurrency(), null, BigDecimal.ZERO, ImmutableList.<UsagePriceOverride>of()));
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ final Entitlement entitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, "bundleExternalKey", overrides, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 1, 1), new LocalDate(2017, 2, 1), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // 2017-02-01
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 2, 1), new LocalDate(2017, 3, 1), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // Do the change mid-month so the repair triggers the bug in https://github.com/killbill/killbill/issues/783
+ entitlement.changePlanWithDate(spec, ImmutableList.<PlanPhasePriceOverride>of(), new LocalDate("2017-02-15"), ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // 2017-02-15
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addDays(15);
+ assertListenerStatus();
+
+ // Note: no repair
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 2, 1), new LocalDate(2017, 3, 1), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 2, 1), new LocalDate(2017, 2, 15), InvoiceItemType.RECURRING, BigDecimal.ZERO),
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 2, 15), new LocalDate(2017, 3, 1), InvoiceItemType.RECURRING, new BigDecimal("14.98")));
+
+ // 2017-03-01
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addDays(15);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 3, 1), new LocalDate(2017, 4, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+ // 2017-04-01
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 5, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2017, 4, 1), new LocalDate(2017, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ }
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java
index 8a04bf8..a8fd0c1 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java
@@ -18,10 +18,13 @@
package org.killbill.billing.beatrix.integration;
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.LocalDate;
+import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.DefaultAccount;
@@ -30,6 +33,7 @@ import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
@@ -39,7 +43,10 @@ import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoiceStatus;
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.invoice.InvoicePaymentControlPluginApi;
+import org.testng.Assert;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
@@ -180,7 +187,7 @@ public class TestIntegrationParentInvoice extends TestIntegrationBase {
// upgrade plan
busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE);
- baseEntitlementChild.changePlanWithDate(new PlanSpecifier("Shotgun", BillingPeriod.MONTHLY, baseEntitlementChild.getLastActivePriceList().getName()), null, null,null, callContext);
+ baseEntitlementChild.changePlanWithDate(new PlanPhaseSpecifier("Shotgun", BillingPeriod.MONTHLY, baseEntitlementChild.getLastActivePriceList().getName()), null, null, null, callContext);
assertListenerStatus();
// check parent invoice. Expected to have the same invoice item with the amount updated
@@ -1128,6 +1135,22 @@ public class TestIntegrationParentInvoice extends TestIntegrationBase {
assertEquals(invoiceUserApi.getAccountBalance(parentAccount.getId(), callContext).compareTo(new BigDecimal("249.95")), 0);
assertEquals(invoiceUserApi.getAccountBalance(childAccount.getId(), callContext).compareTo(new BigDecimal("249.95")), 0);
+
+ // Verify Invoice apis getParentAccountId/getParentInvoiceId
+ assertEquals(childInvoices.get(1).getParentAccountId(), parentAccount.getId());
+ assertEquals(childInvoices.get(1).getParentInvoiceId(), parentInvoice.getId());
+
+ try {
+ final List<PluginProperty> properties = new ArrayList<PluginProperty>();
+ final PluginProperty prop1 = new PluginProperty(InvoicePaymentControlPluginApi.PROP_IPCD_INVOICE_ID, childInvoices.get(1).getId().toString(), false);
+ properties.add(prop1);
+ paymentApi.createPurchaseWithPaymentControl(childAccount, childAccount.getPaymentMethodId(), null, childInvoices.get(1).getBalance(), childInvoices.get(1).getCurrency(), UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), properties, PAYMENT_OPTIONS, callContext);
+ Assert.fail("Payment should fail, invoice belongs to parent");
+ } catch (final PaymentApiException e) {
+ assertEquals(ErrorCode.PAYMENT_PLUGIN_API_ABORTED.getCode(), e.getCode());
+ }
+
final int nbDaysBeforeRetry = paymentConfig.getPaymentFailureRetryDays(internalCallContext).get(0);
// Move time for retry to happen
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoInvoiceDraft.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoInvoiceDraft.java
index 5353676..3956312 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoInvoiceDraft.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoInvoiceDraft.java
@@ -30,12 +30,15 @@ import org.killbill.billing.account.api.Account;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
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.api.InvoiceStatus;
import org.killbill.billing.invoice.api.InvoiceUserApi;
+import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
import org.killbill.billing.util.api.TagApiException;
import org.killbill.billing.util.api.TagDefinitionApiException;
import org.killbill.billing.util.api.TagUserApi;
@@ -74,7 +77,7 @@ public class TestIntegrationWithAutoInvoiceDraft extends TestIntegrationBase {
@Test(groups = "slow")
- public void testBasic() throws Exception {
+ public void testAutoInvoicingDraftBasic() throws Exception {
clock.setTime(new DateTime(2017, 6, 16, 18, 24, 42, 0));
add_AUTO_INVOICING_DRAFT_Tag(account.getId(), ObjectType.ACCOUNT);
@@ -195,4 +198,77 @@ public class TestIntegrationWithAutoInvoiceDraft extends TestIntegrationBase {
}
+
+ @Test(groups = "slow")
+ public void testAutoInvoicingReuseDraftBasic() throws Exception {
+ clock.setTime(new DateTime(2017, 6, 16, 18, 24, 42, 0));
+
+ // Set both AUTO_INVOICING_DRAFT and AUTO_INVOICING_REUSE_DRAFT
+ add_AUTO_INVOICING_DRAFT_Tag(account.getId(), ObjectType.ACCOUNT);
+ add_AUTO_INVOICING_REUSE_DRAFT_Tag(account.getId(), ObjectType.ACCOUNT);
+
+ // Create initial DRAFt invoice that will be reused by the system
+ final LocalDate startDate = clock.getUTCToday();
+ final LocalDate endDate = startDate.plusDays(5);
+ final InvoiceItem externalCharge = new ExternalChargeInvoiceItem(null, account.getId(), null, "Initial external charge", startDate, endDate, BigDecimal.TEN, Currency.USD);
+ invoiceUserApi.insertExternalCharges(account.getId(), clock.getUTCToday(), ImmutableList.<InvoiceItem>of(externalCharge), false, callContext).get(0);
+
+ List<Invoice> invoices;
+ invoices = invoiceApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 1);
+ assertEquals(invoices.get(0).getInvoiceItems().size(), 1);
+ assertEquals(invoices.get(0).getStatus(), InvoiceStatus.DRAFT);
+
+ final UUID invoiceId = invoices.get(0).getId();
+
+ final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK);
+ assertNotNull(bpEntitlement);
+
+ // Verify we see the new item on our existing DRAFT invoice
+ invoices = invoiceApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 1);
+ assertEquals(invoices.get(0).getId(), invoiceId);
+ assertEquals(invoices.get(0).getInvoiceItems().size(), 2);
+ assertEquals(invoices.get(0).getStatus(), InvoiceStatus.DRAFT);
+
+
+ // Move out of TRIAL
+ busHandler.pushExpectedEvents(NextEvent.PHASE);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ // Verify again we see the new item on our existing DRAFT invoice
+ invoices = invoiceApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 1);
+ assertEquals(invoices.get(0).getId(), invoiceId);
+ assertEquals(invoices.get(0).getInvoiceItems().size(), 3);
+ assertEquals(invoices.get(0).getStatus(), InvoiceStatus.DRAFT);
+
+ // Remove AUTO_INVOICING_DRAFT, so next invoicing should commit DRAFt invoice
+ remove_AUTO_INVOICING_DRAFT_Tag(account.getId(), ObjectType.ACCOUNT);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // Verify again we see the new item and this time invoice is in COMMITTED
+ invoices = invoiceApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 1);
+ assertEquals(invoices.get(0).getId(), invoiceId);
+ assertEquals(invoices.get(0).getInvoiceItems().size(), 4);
+ assertEquals(invoices.get(0).getStatus(), InvoiceStatus.COMMITTED);
+
+
+ // Verify we see a new invoice
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoices = invoiceApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 2);
+ assertEquals(invoices.get(1).getInvoiceItems().size(), 1);
+ assertEquals(invoices.get(1).getStatus(), InvoiceStatus.COMMITTED);
+ }
+
+
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
index a57824b..33cef16 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
@@ -116,7 +116,7 @@ public class TestIntegrationWithCatalogUpdate extends TestIntegrationBase {
// Change Plan to the newly added Plan and verify correct default rules behavior (IMMEDIATE change)
busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
- baseEntitlement.changePlan(new PlanSpecifier("SuperFoo", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), testCallContext);
+ baseEntitlement.changePlan(new PlanPhaseSpecifier("SuperFoo", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), testCallContext);
assertListenerStatus();
invoiceChecker.checkInvoice(account.getId(), 2, testCallContext,
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPaymentWithControl.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPaymentWithControl.java
index 7879aad..60bdf05 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPaymentWithControl.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPaymentWithControl.java
@@ -271,6 +271,12 @@ public class TestPaymentWithControl extends TestIntegrationBase {
public UUID getAdjustedPaymentMethodId() {
return adjustedPaymentMethodId;
}
+
+ @Override
+ public String getAdjustedPluginName() {
+ return null;
+ }
+
@Override
public Iterable<PluginProperty> getAdjustedPluginProperties() {
return null;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
index 8ba0800..f8a4499 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
@@ -28,9 +28,14 @@ import org.joda.time.LocalDate;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import org.killbill.billing.catalog.DefaultPlanPhasePriceOverride;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.catalog.api.UsagePriceOverride;
import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
@@ -538,4 +543,197 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, expectedInvoices);
expectedInvoices.clear();
}
+
+ @Test(groups = "slow")
+ public void testBCDChangeFromFreePlanToPayingPlanNoTrial() throws Exception {
+ final PlanPhaseSpecifier specNoTrial = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "notrial", null);
+ testBCDChangeFromFreePlanToPayingPlan(specNoTrial);
+ }
+
+ @Test(groups = "slow")
+ public void testBCDChangeFromFreePlanToPayingPlanWithTrial() throws Exception {
+ // Change to the paying plan (alignment is START_OF_SUBSCRIPTION, but because we are already in an EVERGREEN phase, we will re-align with the EVERGREEN phase of the new 3-phases paying plan)
+ final PlanPhaseSpecifier specWithTrial = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "DEFAULT", null);
+ testBCDChangeFromFreePlanToPayingPlan(specWithTrial);
+ }
+
+ @Test(groups = "slow")
+ public void testBCDChangeFromFreePlanToPayingPlanWithTrialAndCHANGE_OF_PLANPolicy30DaysMonth() throws Exception {
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setTime(initialDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "notrial", null);
+
+ // Price override of $0
+ final List<PlanPhasePriceOverride> overrides = new ArrayList<PlanPhasePriceOverride>();
+ overrides.add(new DefaultPlanPhasePriceOverride("blowdart-monthly-notrial-evergreen", account.getCurrency(), null, BigDecimal.ZERO, ImmutableList.<UsagePriceOverride>of()));
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ // BP creation : Will set Account BCD to the first (DateOfFirstRecurringNonZeroCharge is the subscription start date in this case)
+ final Entitlement baseEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, "bundleExternalKey", overrides, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 1), new LocalDate(2016, 5, 1), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // 2016-4-15
+ clock.addDays(14);
+
+ // Set next BCD to be the 15
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE);
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15, null, internalCallContext);
+ assertListenerStatus();
+
+ // Re-alignment invoice
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 1), new LocalDate(2016, 4, 15), InvoiceItemType.RECURRING, BigDecimal.ZERO),
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 15), new LocalDate(2016, 5, 15), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // Change to the paying plan (alignment is CHANGE_OF_PLAN: we end up in TRIAL)
+ final PlanPhaseSpecifier specWithTrial = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "trial", null);
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE);
+ baseEntitlement.changePlanOverrideBillingPolicy(specWithTrial, ImmutableList.<PlanPhasePriceOverride>of(), clock.getUTCToday(), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Trial invoice
+ invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 15), null, InvoiceItemType.FIXED, BigDecimal.ZERO));
+
+ // Verify next month (extra null invoice because of the original notification set on the 1st)
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // First paying invoice
+ invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 15), new LocalDate(2016, 6, 15), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+ // Verify next month
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 5, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 15), new LocalDate(2016, 7, 15), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ }
+
+ @Test(groups = "slow")
+ public void testBCDChangeFromFreePlanToPayingPlanWithTrialAndCHANGE_OF_PLANPolicy31DaysMonth() throws Exception {
+ final DateTime initialDate = new DateTime(2016, 5, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setTime(initialDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "notrial", null);
+
+ // Price override of $0
+ final List<PlanPhasePriceOverride> overrides = new ArrayList<PlanPhasePriceOverride>();
+ overrides.add(new DefaultPlanPhasePriceOverride("blowdart-monthly-notrial-evergreen", account.getCurrency(), null, BigDecimal.ZERO, ImmutableList.<UsagePriceOverride>of()));
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ // BP creation : Will set Account BCD to the first (DateOfFirstRecurringNonZeroCharge is the subscription start date in this case)
+ final Entitlement baseEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, "bundleExternalKey", overrides, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 1), new LocalDate(2016, 6, 1), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // 2016-5-15
+ clock.addDays(14);
+
+ // Set next BCD to be the 14
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 14, null, internalCallContext);
+ // No bus event, no invoice expected
+ assertListenerStatus();
+
+ // Change to the paying plan (alignment is CHANGE_OF_PLAN: we end up in TRIAL)
+ // Extra NULL_INVOICE event because invoice computes a future notification effective right away
+ final PlanPhaseSpecifier specWithTrial = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "trial", null);
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.NULL_INVOICE, NextEvent.INVOICE);
+ baseEntitlement.changePlanOverrideBillingPolicy(specWithTrial, ImmutableList.<PlanPhasePriceOverride>of(), clock.getUTCToday(), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Trial invoice (with re-alignment invoice item from free plan)
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 1), new LocalDate(2016, 5, 15), InvoiceItemType.RECURRING, BigDecimal.ZERO),
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 15), null, InvoiceItemType.FIXED, BigDecimal.ZERO));
+
+ // Verify next month (extra null invoice because of the original notification set on the 1st)
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // First paying invoice
+ invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 14), new LocalDate(2016, 7, 14), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+ // Verify next month
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 14), new LocalDate(2016, 8, 14), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ }
+
+ private void testBCDChangeFromFreePlanToPayingPlan(final PlanPhaseSpecifier toSpec) throws Exception {
+ final DateTime initialDate = new DateTime(2016, 4, 1, 0, 13, 42, 0, testTimeZone);
+ clock.setTime(initialDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Blowdart", BillingPeriod.MONTHLY, "notrial", null);
+
+ // Price override of $0
+ final List<PlanPhasePriceOverride> overrides = new ArrayList<PlanPhasePriceOverride>();
+ overrides.add(new DefaultPlanPhasePriceOverride("blowdart-monthly-notrial-evergreen", account.getCurrency(), null, BigDecimal.ZERO, ImmutableList.<UsagePriceOverride>of()));
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ // BP creation : Will set Account BCD to the first (DateOfFirstRecurringNonZeroCharge is the subscription start date in this case)
+ final Entitlement baseEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, "bundleExternalKey", overrides, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 1), new LocalDate(2016, 5, 1), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // 2016-4-15
+ clock.addDays(14);
+
+ // Set next BCD to be the 15
+ busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE);
+ subscriptionBaseInternalApi.updateBCD(baseEntitlement.getId(), 15, null, internalCallContext);
+ assertListenerStatus();
+
+ // Re-alignment invoice
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 1), new LocalDate(2016, 4, 15), InvoiceItemType.RECURRING, BigDecimal.ZERO),
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 15), new LocalDate(2016, 5, 15), InvoiceItemType.RECURRING, BigDecimal.ZERO));
+
+ // Change to the paying plan
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ baseEntitlement.changePlanOverrideBillingPolicy(toSpec, ImmutableList.<PlanPhasePriceOverride>of(), clock.getUTCToday(), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // First paying invoice
+ invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 15), new LocalDate(2016, 5, 15), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+ // Verify next month (null invoice because of the original notification set on the 1st)
+ busHandler.pushExpectedEvents(NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 15), new LocalDate(2016, 6, 15), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+ // Verify next month
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 5, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 15), new LocalDate(2016, 7, 15), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+ }
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithCatalogPlugin.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithCatalogPlugin.java
index a67a191..134819d 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithCatalogPlugin.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithCatalogPlugin.java
@@ -207,7 +207,7 @@ public class TestWithCatalogPlugin extends TestIntegrationBase {
public VersionedPluginCatalog getVersionedPluginCatalog(final Iterable<PluginProperty> properties, final TenantContext tenantContext) {
nbVersionedPluginCatalogApiCalls++;
Assert.assertNotNull(versionedCatalog, "test did not initialize plugin catalog");
- return new TestModelVersionedPluginCatalog(versionedCatalog.getCatalogName(), versionedCatalog.getRecurringBillingMode(), toStandalonePluginCatalogs(versionedCatalog.getVersions()));
+ return new TestModelVersionedPluginCatalog(versionedCatalog.getCatalogName(), toStandalonePluginCatalogs(versionedCatalog.getVersions()));
}
public void addCatalogVersion(final String catalogResource) throws Exception {
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithPriceOverride.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithPriceOverride.java
index bc7a72f..0091248 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithPriceOverride.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithPriceOverride.java
@@ -26,20 +26,17 @@ 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.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
-import org.killbill.billing.beatrix.util.PaymentChecker.ExpectedPaymentCheck;
import org.killbill.billing.catalog.DefaultPlanPhasePriceOverride;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
-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.PlanSpecifier;
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.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.payment.api.PluginProperty;
-import org.killbill.billing.payment.api.TransactionStatus;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
@@ -121,7 +118,7 @@ public class TestWithPriceOverride extends TestIntegrationBase {
overrides.add(new DefaultPlanPhasePriceOverride("shotgun-monthly-evergreen", account.getCurrency(), null, new BigDecimal("279.95"), null));
busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
- bpSubscription.changePlanOverrideBillingPolicy(new PlanSpecifier("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), overrides, null, BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
+ bpSubscription.changePlanOverrideBillingPolicy(new PlanPhaseSpecifier("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), overrides, null, BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
assertListenerStatus();
invoiceChecker.checkInvoice(account.getId(), 3, callContext,
@@ -147,7 +144,7 @@ public class TestWithPriceOverride extends TestIntegrationBase {
final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
accountChecker.checkAccount(account.getId(), accountData, callContext);
- final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, 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")));
@@ -168,7 +165,7 @@ public class TestWithPriceOverride extends TestIntegrationBase {
overrides.add(new DefaultPlanPhasePriceOverride("telescopic-scope-monthly-evergreen", account.getCurrency(), null, new BigDecimal("1200.00"), null));
busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
- aoEntitlement.changePlanOverrideBillingPolicy(new PlanSpecifier("Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), overrides, null, BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
+ aoEntitlement.changePlanOverrideBillingPolicy(new PlanPhaseSpecifier("Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), overrides, null, BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
assertListenerStatus();
}
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
index 245440f..0373e89 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
@@ -22,6 +22,7 @@ import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
+import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.xml.bind.JAXBException;
import javax.xml.transform.TransformerException;
@@ -167,14 +168,14 @@ public class DefaultCatalogUserApi implements CatalogUserApi {
@Override
- public void createDefaultEmptyCatalog(final DateTime effectiveDate, final CallContext callContext) throws CatalogApiException {
+ public void createDefaultEmptyCatalog(@Nullable final DateTime effectiveDate, final CallContext callContext) throws CatalogApiException {
try {
final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContextWithoutAccountRecordId(callContext);
final StandaloneCatalog currentCatalog = getCurrentStandaloneCatalogForTenant(internalTenantContext);
final CatalogUpdater catalogUpdater = (currentCatalog != null) ?
- new CatalogUpdater(currentCatalog) :
- new CatalogUpdater(BillingMode.IN_ADVANCE, effectiveDate, null);
+ new CatalogUpdater(currentCatalog) :
+ new CatalogUpdater(getSafeFirstCatalogEffectiveDate(effectiveDate), null);
catalogCache.clearCatalog(internalTenantContext);
tenantApi.updateTenantKeyValue(TenantKey.CATALOG.toString(), catalogUpdater.getCatalogXML(), callContext);
@@ -184,15 +185,14 @@ public class DefaultCatalogUserApi implements CatalogUserApi {
}
@Override
- public void addSimplePlan(final SimplePlanDescriptor descriptor, final DateTime effectiveDate, final CallContext callContext) throws CatalogApiException {
+ public void addSimplePlan(final SimplePlanDescriptor descriptor, @Nullable final DateTime effectiveDate, final CallContext callContext) throws CatalogApiException {
try {
final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContextWithoutAccountRecordId(callContext);
final StandaloneCatalog currentCatalog = getCurrentStandaloneCatalogForTenant(internalTenantContext);
final CatalogUpdater catalogUpdater = (currentCatalog != null) ?
- new CatalogUpdater(currentCatalog) :
- new CatalogUpdater(BillingMode.IN_ADVANCE, effectiveDate, descriptor.getCurrency());
-
+ new CatalogUpdater(currentCatalog) :
+ new CatalogUpdater(getSafeFirstCatalogEffectiveDate(effectiveDate), descriptor.getCurrency());
catalogUpdater.addSimplePlanDescriptor(descriptor);
catalogCache.clearCatalog(internalTenantContext);
@@ -214,6 +214,12 @@ public class DefaultCatalogUserApi implements CatalogUserApi {
throw new CatalogApiException(e);
}
}
+ private DateTime getSafeFirstCatalogEffectiveDate(@Nullable final DateTime input) {
+ // The effectiveDate for the initial version does not matter too much
+ // Because of #760, we want to make that client passing a approximate date (e.g today.toDateTimeAtStartOfDay()) will find the version
+ final DateTime catalogEffectiveDate = clock.getUTCNow().minusDays(1);
+ return (input == null || input.isAfter(catalogEffectiveDate)) ? catalogEffectiveDate : input;
+ }
private StandaloneCatalog getCurrentStandaloneCatalogForTenant(final InternalTenantContext internalTenantContext) throws CatalogApiException {
final VersionedCatalog versionedCatalog = (VersionedCatalog) catalogService.getCurrentCatalog(false, false, internalTenantContext);
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java b/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
index 154949e..6f53186 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
@@ -68,15 +68,17 @@ public class CatalogUpdater {
public CatalogUpdater(final StandaloneCatalog standaloneCatalog) {
this.catalog = new DefaultMutableStaticCatalog(standaloneCatalog);
+ this.catalog.setRecurringBillingMode(BillingMode.IN_ADVANCE);
+
}
- public CatalogUpdater(final BillingMode billingMode, final DateTime effectiveDate, final Currency... currencies) {
+ public CatalogUpdater(final DateTime effectiveDate, final Currency... currencies) {
final DefaultPriceList defaultPriceList = new DefaultPriceList().setName(PriceListSet.DEFAULT_PRICELIST_NAME);
final StandaloneCatalog tmp = new StandaloneCatalog()
.setCatalogName(DEFAULT_CATALOG_NAME)
.setEffectiveDate(effectiveDate.toDate())
- .setRecurringBillingMode(billingMode)
+ .setRecurringBillingMode(BillingMode.IN_ADVANCE)
.setProducts(ImmutableList.<Product>of())
.setPlans(ImmutableList.<Plan>of())
.setPriceLists(new DefaultPriceListSet(defaultPriceList, new DefaultPriceList[0]))
@@ -132,6 +134,7 @@ public class CatalogUpdater {
plan.setName(desc.getPlanId());
plan.setPriceListName(PriceListSet.DEFAULT_PRICELIST_NAME);
plan.setProduct(product);
+ plan.setRecurringBillingMode(catalog.getRecurringBillingMode());
if (desc.getTrialLength() > 0 && desc.getTrialTimeUnit() != TimeUnit.UNLIMITED) {
final DefaultPlanPhase trialPhase = new DefaultPlanPhase();
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultMutableStaticCatalog.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultMutableStaticCatalog.java
index 8e83016..c6d9a6c 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultMutableStaticCatalog.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultMutableStaticCatalog.java
@@ -42,12 +42,12 @@ public class DefaultMutableStaticCatalog extends StandaloneCatalog implements Mu
public DefaultMutableStaticCatalog(final StandaloneCatalog input) {
this.setCatalogName(input.getCatalogName())
+ .setRecurringBillingMode(input.getRecurringBillingMode())
.setEffectiveDate(input.getEffectiveDate())
.setSupportedCurrencies(input.getCurrentSupportedCurrencies())
.setUnits(input.getCurrentUnits())
.setProducts(input.getCurrentProducts())
.setPlans(input.getCurrentPlans())
- .setRecurringBillingMode(input.getRecurringBillingMode())
.setPlanRules(input.getPlanRules())
.setPriceLists(input.getPriceLists());
initialize(this, null);
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
index cf7c9a3..a61fa3c 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
@@ -40,6 +40,7 @@ import javax.xml.bind.annotation.XmlIDREF;
import org.joda.time.DateTime;
import org.killbill.billing.ErrorCode;
+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.PhaseType;
@@ -76,6 +77,10 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
@XmlIDREF
private DefaultProduct product;
+ // In order to support older versions where recurringBillingMode is defined at the StandaloneCatalog level, we don't require that field.
+ @XmlElement(required = false)
+ private BillingMode recurringBillingMode;
+
@XmlElementWrapper(name = "initialPhases", required = false)
@XmlElement(name = "phase", required = false)
private DefaultPlanPhase[] initialPhases;
@@ -108,6 +113,7 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
}
this.finalPhase = new DefaultPlanPhase(this, in.getFinalPhase(), overrides[overrides.length - 1]);
this.priceListName = in.getPriceListName();
+ this.recurringBillingMode = in.getRecurringBillingMode();
}
@Override
@@ -115,6 +121,10 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
return effectiveDateForExistingSubscriptions;
}
+ @Override
+ public BillingMode getRecurringBillingMode() {
+ return recurringBillingMode;
+ }
@Override
public DefaultPlanPhase[] getInitialPhases() {
@@ -204,6 +214,10 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
p.setPlan(this);
p.initialize(catalog, sourceURI);
}
+ if (recurringBillingMode == null) {
+ this.recurringBillingMode = catalog.getRecurringBillingMode();
+ }
+
this.priceListName = this.priceListName != null ? this.priceListName : findPriceListForPlan(catalog);
}
@@ -217,13 +231,17 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
catalog.getCatalogURI(), DefaultPlan.class, ""));
}
+ if (recurringBillingMode == null) {
+ errors.add(new ValidationError(String.format("Invalid reccuring billingMode for plan '%s'", name), catalog.getCatalogURI(), DefaultPlan.class, ""));
+ }
+
if (product == null) {
errors.add(new ValidationError(String.format("Invalid product for plan '%s'", name), catalog.getCatalogURI(), DefaultPlan.class, ""));
}
for (final DefaultPlanPhase cur : initialPhases) {
cur.validate(catalog, errors);
- if (cur.getPhaseType() == PhaseType.EVERGREEN || cur.getPhaseType() == PhaseType.FIXEDTERM) {
+ if (cur.getPhaseType() == PhaseType.EVERGREEN) {
errors.add(new ValidationError(String.format("Initial Phase %s of plan %s cannot be of type %s",
cur.getName(), name, cur.getPhaseType()),
catalog.getCatalogURI(), DefaultPlan.class, ""));
@@ -284,6 +302,11 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
return this;
}
+ public DefaultPlan setRecurringBillingMode(final BillingMode billingMode) {
+ this.recurringBillingMode = billingMode;
+ return this;
+ }
+
@Override
public DateTime dateOfFirstRecurringNonZeroCharge(final DateTime subscriptionStartDate, final PhaseType initialPhaseType) {
DateTime result = subscriptionStartDate;
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/plugin/StandaloneCatalogMapper.java b/catalog/src/main/java/org/killbill/billing/catalog/plugin/StandaloneCatalogMapper.java
index 4d715cf..c8bf3d4 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/plugin/StandaloneCatalogMapper.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/plugin/StandaloneCatalogMapper.java
@@ -44,7 +44,6 @@ import org.killbill.billing.catalog.DefaultTieredBlock;
import org.killbill.billing.catalog.DefaultUnit;
import org.killbill.billing.catalog.DefaultUsage;
import org.killbill.billing.catalog.StandaloneCatalog;
-import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.Block;
import org.killbill.billing.catalog.api.CurrencyValueNull;
import org.killbill.billing.catalog.api.Duration;
@@ -91,16 +90,13 @@ import com.google.common.collect.Iterables;
public class StandaloneCatalogMapper {
private final String catalogName;
- private final BillingMode recurringBillingMode;
-
private Map<String, Product> tmpDefaultProducts;
private Map<String, Plan> tmpDefaultPlans;
private DefaultPriceListSet tmpDefaultPriceListSet;
private Map<String, DefaultPriceList> tmpDefaultPriceListMap;
- public StandaloneCatalogMapper(final String catalogName, final BillingMode recurringBillingMode) {
+ public StandaloneCatalogMapper(final String catalogName) {
this.catalogName = catalogName;
- this.recurringBillingMode = recurringBillingMode;
this.tmpDefaultProducts = null;
this.tmpDefaultPlans = null;
this.tmpDefaultPriceListMap = new HashMap<String, DefaultPriceList>();
@@ -114,7 +110,6 @@ public class StandaloneCatalogMapper {
result.setProducts(toDefaultProducts(pluginCatalog.getProducts()));
result.setPlans(toDefaultPlans(pluginCatalog.getPlans()));
result.setPriceLists(toDefaultPriceListSet(pluginCatalog.getDefaultPriceList(), pluginCatalog.getChildrenPriceList()));
- result.setRecurringBillingMode(recurringBillingMode);
result.setSupportedCurrencies(toArray(pluginCatalog.getCurrencies()));
result.setUnits(toDefaultUnits(pluginCatalog.getUnits()));
result.setPlanRules(toDefaultPlanRules(pluginCatalog.getPlanRules()));
@@ -403,6 +398,7 @@ public class StandaloneCatalogMapper {
final DefaultPlan result = new DefaultPlan();
result.setName(input.getName());
result.setPrettyName(input.getPrettyName());
+ result.setRecurringBillingMode(input.getRecurringBillingMode());
result.setEffectiveDateForExistingSubscriptions(input.getEffectiveDateForExistingSubscriptions());
result.setFinalPhase(toDefaultPlanPhase(input.getFinalPhase()));
result.setInitialPhases(toDefaultPlanPhases(ImmutableList.copyOf(input.getInitialPhases())));
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/plugin/VersionedCatalogMapper.java b/catalog/src/main/java/org/killbill/billing/catalog/plugin/VersionedCatalogMapper.java
index 1ee8470..eb170ce 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/plugin/VersionedCatalogMapper.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/plugin/VersionedCatalogMapper.java
@@ -59,7 +59,7 @@ public class VersionedCatalogMapper {
}
private StandaloneCatalogWithPriceOverride toStandaloneCatalogWithPriceOverride(final VersionedPluginCatalog pluginCatalog, final StandalonePluginCatalog input, final InternalTenantContext internalTenantContext) {
- final StandaloneCatalogMapper mapper = new StandaloneCatalogMapper(pluginCatalog.getCatalogName(), pluginCatalog.getRecurringBillingMode());
+ final StandaloneCatalogMapper mapper = new StandaloneCatalogMapper(pluginCatalog.getCatalogName());
final StandaloneCatalog catalog = mapper.toStandaloneCatalog(input, null);
final StandaloneCatalogWithPriceOverride result = new StandaloneCatalogWithPriceOverride(catalog, priceOverride, internalTenantContext.getTenantRecordId(), internalCallContextFactory);
return result;
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalog.java b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalog.java
index 96869e4..db734df 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalog.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalog.java
@@ -65,7 +65,7 @@ public class StandaloneCatalog extends ValidatingConfig<StandaloneCatalog> imple
@XmlElement(required = true)
private String catalogName;
- @XmlElement(required = true)
+ @XmlElement(required = false)
private BillingMode recurringBillingMode;
@XmlElementWrapper(name = "currencies", required = true)
@@ -114,11 +114,6 @@ public class StandaloneCatalog extends ValidatingConfig<StandaloneCatalog> imple
return effectiveDate;
}
- @Override
- public BillingMode getRecurringBillingMode() {
- return recurringBillingMode;
- }
-
/* (non-Javadoc)
* @see org.killbill.billing.catalog.ICatalog#getProducts()
*/
@@ -316,6 +311,9 @@ public class StandaloneCatalog extends ValidatingConfig<StandaloneCatalog> imple
}
}
+ public BillingMode getRecurringBillingMode() {
+ return recurringBillingMode;
+ }
//////////////////////////////////////////////////////////////////////////////
//
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
index d7a2d9b..f78480d 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
@@ -49,7 +49,6 @@ public class StandaloneCatalogWithPriceOverride extends StandaloneCatalog implem
// Initialize from input catalog
setCatalogName(catalog.getCatalogName());
setEffectiveDate(catalog.getEffectiveDate());
- setRecurringBillingMode(catalog.getRecurringBillingMode());
setProducts(catalog.getCurrentProducts());
setPlans(catalog.getCurrentPlans());
setPriceLists(catalog.getPriceLists());
@@ -75,8 +74,7 @@ public class StandaloneCatalogWithPriceOverride extends StandaloneCatalog implem
final Plan defaultPlan = super.createOrFindCurrentPlan(spec, null);
if (overrides == null ||
overrides.getOverrides() == null ||
- overrides.getOverrides().isEmpty() ||
- isOverrideUsedForPlanAlignmentTargetPhaseType(overrides)) {
+ overrides.getOverrides().isEmpty()) {
return defaultPlan;
}
@@ -84,21 +82,6 @@ public class StandaloneCatalogWithPriceOverride extends StandaloneCatalog implem
return priceOverride.getOrCreateOverriddenPlan(this, defaultPlan, CatalogDateHelper.toUTCDateTime(getEffectiveDate()), overrides.getOverrides(), internalCallContext);
}
- // This is a hack used to specify a target PhaseType when making a changePlan operation. Undocumented feature
- private boolean isOverrideUsedForPlanAlignmentTargetPhaseType(final PlanPhasePriceOverridesWithCallContext overrides) {
- if (overrides.getOverrides().size() != 1) {
- return false;
- }
-
- final PlanPhasePriceOverride override = overrides.getOverrides().get(0);
- return override.getCurrency() == null &&
- override.getFixedPrice() == null &&
- override.getRecurringPrice() == null &&
- override.getPhaseName() == null &&
- override.getPlanPhaseSpecifier() != null &&
- override.getPlanPhaseSpecifier().getPhaseType() != null;
- }
-
@Override
public DefaultPlan findCurrentPlan(final String planName) throws CatalogApiException {
final Matcher m = DefaultPriceOverride.CUSTOM_PLAN_NAME_PATTERN.matcher(planName);
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java b/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
index c193b16..73dfb28 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
@@ -85,9 +85,6 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
@XmlElement(required = true)
private String catalogName;
- @XmlElement(required = true)
- private BillingMode recurringBillingMode;
-
// Required for JAXB deserialization
public VersionedCatalog() {
this.clock = null;
@@ -181,8 +178,11 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
}
}
+
+ final boolean initialVersion = (i == 0);
final DateTime catalogEffectiveDate = CatalogDateHelper.toUTCDateTime(c.getEffectiveDate());
- if (!subscriptionStartDate.isBefore(catalogEffectiveDate)) { // Its a new subscription this plan always applies
+ if (initialVersion || // Prevent issue with time granularity -- see #760
+ !subscriptionStartDate.isBefore(catalogEffectiveDate)) { // It's a new subscription this plan always applies
return new CatalogPlanEntry(c, plan);
} else { //Its an existing subscription
if (plan.getEffectiveDateForExistingSubscriptions() != null) { //if it is null any change to this does not apply to existing subscriptions
@@ -243,9 +243,6 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
if (catalogName == null && e.getCatalogName() != null) {
catalogName = e.getCatalogName();
}
- if (recurringBillingMode == null) {
- recurringBillingMode = e.getRecurringBillingMode();
- }
versions.add(e);
Collections.sort(versions, new Comparator<StandaloneCatalog>() {
@Override
@@ -444,10 +441,6 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
errors.add(new ValidationError(String.format("Catalog name '%s' is not consistent across versions ", c.getCatalogName()),
c.getCatalogURI(), VersionedCatalog.class, ""));
}
- if (!c.getRecurringBillingMode().equals(recurringBillingMode)) {
- errors.add(new ValidationError(String.format("Catalog recurringBillingMode '%s' is not consistent across versions ", c.getCatalogName()),
- c.getCatalogURI(), VersionedCatalog.class, ""));
- }
errors.addAll(c.validate(c, errors));
}
return errors;
@@ -467,11 +460,6 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
}
@Override
- public BillingMode getRecurringBillingMode() {
- return recurringBillingMode;
- }
-
- @Override
public Currency[] getCurrentSupportedCurrencies() throws CatalogApiException {
return versionForDate(clock.getUTCNow()).getCurrentSupportedCurrencies();
}
diff --git a/catalog/src/main/resources/org/killbill/billing/catalog/ddl.sql b/catalog/src/main/resources/org/killbill/billing/catalog/ddl.sql
index 0bbd08d..dcae41e 100644
--- a/catalog/src/main/resources/org/killbill/billing/catalog/ddl.sql
+++ b/catalog/src/main/resources/org/killbill/billing/catalog/ddl.sql
@@ -32,7 +32,7 @@ CREATE INDEX catalog_override_phase_definition_idx ON catalog_override_phase_def
DROP TABLE IF EXISTS catalog_override_plan_phase;
CREATE TABLE catalog_override_plan_phase (
record_id serial unique,
- phase_number smallint /*! unsigned */ NOT NULL,
+ phase_number int /*! unsigned */ NOT NULL,
phase_def_record_id bigint /*! unsigned */ not null,
target_plan_def_record_id bigint /*! unsigned */ not null,
created_date datetime NOT NULL,
@@ -80,8 +80,8 @@ create table catalog_override_block_definition
(
record_id serial unique,
parent_unit_name varchar(255) NOT NULL,
-size double NOT NULL,
-max double NULL,
+size decimal(15,9) NOT NULL,
+max decimal(15,9) NULL,
currency varchar(3) NOT NULL,
price decimal(15,9) NOT NULL,
effective_date datetime NOT NULL,
@@ -97,7 +97,7 @@ DROP TABLE IF EXISTS catalog_override_phase_usage;
create table catalog_override_phase_usage
(
record_id serial unique,
-usage_number smallint(5) unsigned,
+usage_number int /*! unsigned */,
usage_def_record_id bigint /*! unsigned */ not null,
target_phase_def_record_id bigint /*! unsigned */ not null,
created_date datetime NOT NULL,
@@ -111,7 +111,7 @@ DROP TABLE IF EXISTS catalog_override_usage_tier;
create table catalog_override_usage_tier
(
record_id serial unique,
-tier_number smallint(5) unsigned,
+tier_number int /*! unsigned */,
tier_def_record_id bigint /*! unsigned */ not null,
target_usage_def_record_id bigint /*! unsigned */ not null,
created_date datetime NOT NULL,
@@ -126,7 +126,7 @@ DROP TABLE IF EXISTS catalog_override_tier_block;
create table catalog_override_tier_block
(
record_id serial unique,
-block_number smallint(5) unsigned,
+block_number int /*! unsigned */,
block_def_record_id bigint /*! unsigned */ not null,
target_tier_def_record_id bigint /*! unsigned */ not null,
created_date datetime NOT NULL,
diff --git a/catalog/src/main/resources/org/killbill/billing/catalog/migration/V20161220000000__unit_price_override.sql b/catalog/src/main/resources/org/killbill/billing/catalog/migration/V20161220000000__unit_price_override.sql
index 85f5589..0abe200 100644
--- a/catalog/src/main/resources/org/killbill/billing/catalog/migration/V20161220000000__unit_price_override.sql
+++ b/catalog/src/main/resources/org/killbill/billing/catalog/migration/V20161220000000__unit_price_override.sql
@@ -15,6 +15,7 @@ PRIMARY KEY(record_id)
);
CREATE INDEX catalog_override_usage_definition_idx ON catalog_override_usage_definition(tenant_record_id, parent_usage_name, currency);
+
DROP TABLE IF EXISTS catalog_override_tier_definition;
create table catalog_override_tier_definition
(
@@ -35,8 +36,8 @@ create table catalog_override_block_definition
(
record_id serial unique,
parent_unit_name varchar(255) NOT NULL,
-size double NOT NULL,
-max double NULL,
+size decimal(15,9) NOT NULL,
+max decimal(15,9) NULL,
currency varchar(3) NOT NULL,
price decimal(15,9) NOT NULL,
effective_date datetime NOT NULL,
@@ -52,7 +53,7 @@ DROP TABLE IF EXISTS catalog_override_phase_usage;
create table catalog_override_phase_usage
(
record_id serial unique,
-usage_number smallint(5) unsigned,
+usage_number int /*! unsigned */,
usage_def_record_id bigint /*! unsigned */ not null,
target_phase_def_record_id bigint /*! unsigned */ not null,
created_date datetime NOT NULL,
@@ -66,7 +67,7 @@ DROP TABLE IF EXISTS catalog_override_usage_tier;
create table catalog_override_usage_tier
(
record_id serial unique,
-tier_number smallint(5) unsigned,
+tier_number int /*! unsigned */,
tier_def_record_id bigint /*! unsigned */ not null,
target_usage_def_record_id bigint /*! unsigned */ not null,
created_date datetime NOT NULL,
@@ -81,7 +82,7 @@ DROP TABLE IF EXISTS catalog_override_tier_block;
create table catalog_override_tier_block
(
record_id serial unique,
-block_number smallint(5) unsigned,
+block_number int /*! unsigned */,
block_def_record_id bigint /*! unsigned */ not null,
target_tier_def_record_id bigint /*! unsigned */ not null,
created_date datetime NOT NULL,
@@ -89,6 +90,4 @@ created_by varchar(50) NOT NULL,
tenant_record_id bigint /*! unsigned */ NOT NULL default 0,
PRIMARY KEY(record_id)
);
-CREATE INDEX catalog_override_tier_block_idx ON catalog_override_tier_block(tenant_record_id, block_number, block_def_record_id);
-
-
+CREATE INDEX catalog_override_tier_block_idx ON catalog_override_tier_block(tenant_record_id, block_number, block_def_record_id);
\ No newline at end of file
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLWriter.java b/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLWriter.java
index b431e9a..7a3c905 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLWriter.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLWriter.java
@@ -36,6 +36,7 @@ import org.killbill.billing.catalog.DefaultRecurring;
import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.StandaloneCatalogWithPriceOverride;
import org.killbill.billing.catalog.VersionedCatalog;
+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.MutableStaticCatalog;
@@ -111,6 +112,7 @@ public class TestXMLWriter extends CatalogTestSuiteNoDB {
newPlan.setProduct(newProduct);
newPlan.setInitialPhases(new DefaultPlanPhase[]{trialPhase});
newPlan.setFinalPhase(evergreenPhase);
+ newPlan.setRecurringBillingMode(BillingMode.IN_ADVANCE);
// TODO Ordering breaks
mutableCatalog.addPlan(newPlan);
newPlan.initialize((StandaloneCatalog) mutableCatalog, new URI("dummy"));
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/MockPlan.java b/catalog/src/test/java/org/killbill/billing/catalog/MockPlan.java
index 54a95ba..9fde075 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/MockPlan.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/MockPlan.java
@@ -18,6 +18,7 @@ package org.killbill.billing.catalog;
import java.util.Collection;
+import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.Plan;
import com.google.common.collect.ImmutableList;
@@ -94,6 +95,7 @@ public class MockPlan extends DefaultPlan {
setFinalPhase(finalPhase);
setInitialPhases(planPhases);
setPlansAllowedInBundle(plansAllowedInBundle);
+ setRecurringBillingMode(BillingMode.IN_ADVANCE);
finalPhase.setPlan(this);
for (final DefaultPlanPhase pp : planPhases) {
@@ -113,7 +115,7 @@ public class MockPlan extends DefaultPlan {
setName("Test");
setProduct(MockProduct.createBicycle());
setFinalPhase(mockPlanPhase);
-
+ setRecurringBillingMode(BillingMode.IN_ADVANCE);
mockPlanPhase.setPlan(this);
}
@@ -122,6 +124,7 @@ public class MockPlan extends DefaultPlan {
setProduct(new MockProduct());
setFinalPhase(new MockPlanPhase(this));
setInitialPhases(null);
+ setRecurringBillingMode(BillingMode.IN_ADVANCE);
setPlansAllowedInBundle(1);
}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/plugin/TestCatalogPluginMapping.java b/catalog/src/test/java/org/killbill/billing/catalog/plugin/TestCatalogPluginMapping.java
index ff48895..ed37579 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/plugin/TestCatalogPluginMapping.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/plugin/TestCatalogPluginMapping.java
@@ -46,9 +46,10 @@ public class TestCatalogPluginMapping extends CatalogTestSuiteNoDB {
final StandaloneCatalog inputCatalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
final StandalonePluginCatalog pluginCatalog = buildStandalonePluginCatalog(inputCatalog);
- final StandaloneCatalogMapper mapper = new StandaloneCatalogMapper(inputCatalog.getCatalogName(), inputCatalog.getRecurringBillingMode());
+ final StandaloneCatalogMapper mapper = new StandaloneCatalogMapper(inputCatalog.getCatalogName());
final StandaloneCatalog output = mapper.toStandaloneCatalog(pluginCatalog, inputCatalog.getCatalogURI());
+ output.setRecurringBillingMode(inputCatalog.getRecurringBillingMode());
Assert.assertEquals(output, inputCatalog);
}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/plugin/TestModelVersionedPluginCatalog.java b/catalog/src/test/java/org/killbill/billing/catalog/plugin/TestModelVersionedPluginCatalog.java
index 18de96f..660e234 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/plugin/TestModelVersionedPluginCatalog.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/plugin/TestModelVersionedPluginCatalog.java
@@ -24,15 +24,11 @@ import org.killbill.billing.catalog.plugin.api.VersionedPluginCatalog;
public class TestModelVersionedPluginCatalog implements VersionedPluginCatalog {
private final String catalogName;
- private final BillingMode billingMode;
-
private final Iterable<StandalonePluginCatalog> standalonePluginCatalogs;
public TestModelVersionedPluginCatalog(final String catalogName,
- final BillingMode billingMode,
final Iterable<StandalonePluginCatalog> standalonePluginCatalogs) {
this.catalogName = catalogName;
- this.billingMode = billingMode;
this.standalonePluginCatalogs = standalonePluginCatalogs;
}
@@ -41,10 +37,6 @@ public class TestModelVersionedPluginCatalog implements VersionedPluginCatalog {
return catalogName;
}
- @Override
- public BillingMode getRecurringBillingMode() {
- return billingMode;
- }
@Override
public Iterable<StandalonePluginCatalog> getStandalonePluginCatalogs() {
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
index f0707ed..585e2b0 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
@@ -39,6 +39,7 @@ import org.killbill.billing.catalog.api.TimeUnit;
import org.killbill.billing.catalog.api.user.DefaultSimplePlanDescriptor;
import org.killbill.xmlloader.XMLLoader;
import org.killbill.xmlloader.XMLWriter;
+import org.testng.Assert;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
@@ -55,7 +56,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
final DateTime now = clock.getUTCNow();
- final CatalogUpdater catalogUpdater = new CatalogUpdater(BillingMode.IN_ADVANCE, now, null);
+ final CatalogUpdater catalogUpdater = new CatalogUpdater(now, null);
final String catalogXML = catalogUpdater.getCatalogXML();
final StandaloneCatalog catalog = XMLLoader.getObjectFromStream(new URI("dummy"), new ByteArrayInputStream(catalogXML.getBytes(Charset.forName("UTF-8"))), StandaloneCatalog.class);
assertEquals(catalog.getCurrentPlans().size(), 0);
@@ -67,7 +68,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
final DateTime now = clock.getUTCNow();
final SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor("foo-monthly", "Foo", ProductCategory.BASE, Currency.EUR, BigDecimal.TEN, BillingPeriod.MONTHLY, 0, TimeUnit.UNLIMITED, ImmutableList.<String>of());
- final CatalogUpdater catalogUpdater = new CatalogUpdater(BillingMode.IN_ADVANCE, now, desc.getCurrency());
+ final CatalogUpdater catalogUpdater = new CatalogUpdater(now, desc.getCurrency());
catalogUpdater.addSimplePlanDescriptor(desc);
@@ -108,7 +109,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
final DateTime now = clock.getUTCNow();
final SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor("foo-monthly", "Foo", ProductCategory.BASE, Currency.EUR, BigDecimal.TEN, BillingPeriod.MONTHLY, 14, TimeUnit.DAYS, ImmutableList.<String>of());
- final CatalogUpdater catalogUpdater = new CatalogUpdater(BillingMode.IN_ADVANCE, now, desc.getCurrency());
+ final CatalogUpdater catalogUpdater = new CatalogUpdater(now, desc.getCurrency());
catalogUpdater.addSimplePlanDescriptor(desc);
@@ -258,6 +259,59 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
@Test(groups = "fast")
+ public void testPlanWithNonFinalFixedTermPhase() throws Exception {
+
+
+ final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class);
+
+ final MutableStaticCatalog mutableCatalog = new DefaultMutableStaticCatalog(catalog);
+
+ final DefaultProduct newProduct = new DefaultProduct();
+ newProduct.setName("Something");
+ newProduct.setCatagory(ProductCategory.BASE);
+ newProduct.initialize((StandaloneCatalog) mutableCatalog, null);
+ mutableCatalog.addProduct(newProduct);
+
+
+ final DefaultPlanPhase trialPhase = new DefaultPlanPhase();
+ trialPhase.setPhaseType(PhaseType.TRIAL);
+ trialPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.DAYS).setNumber(14));
+ trialPhase.setFixed(new DefaultFixed().setFixedPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.ZERO)})));
+
+ // Add a Plan with a FIXEDTERM phase
+ final DefaultPlanPhase fixedTermPhase = new DefaultPlanPhase();
+ fixedTermPhase.setPhaseType(PhaseType.FIXEDTERM);
+ fixedTermPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.MONTHS).setNumber(3));
+ fixedTermPhase.setRecurring(new DefaultRecurring().setBillingPeriod(BillingPeriod.MONTHLY).setRecurringPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.TEN)})));
+
+
+ final DefaultPlanPhase evergreenPhase = new DefaultPlanPhase();
+ evergreenPhase.setPhaseType(PhaseType.EVERGREEN);
+ evergreenPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.MONTHS).setNumber(1));
+ evergreenPhase.setRecurring(new DefaultRecurring().setBillingPeriod(BillingPeriod.MONTHLY).setRecurringPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.TEN)})));
+
+
+ final DefaultPlan newPlan = new DefaultPlan();
+ newPlan.setName("something-with-fixed-term");
+ newPlan.setPriceListName(DefaultPriceListSet.DEFAULT_PRICELIST_NAME);
+ newPlan.setProduct(newProduct);
+ newPlan.setInitialPhases(new DefaultPlanPhase[] {trialPhase, fixedTermPhase});
+ newPlan.setFinalPhase(fixedTermPhase);
+ mutableCatalog.addPlan(newPlan);
+ newPlan.initialize((StandaloneCatalog) mutableCatalog, new URI("dummy"));
+
+ final String newCatalogStr = XMLWriter.writeXML((StandaloneCatalog) mutableCatalog, StandaloneCatalog.class);
+ final StandaloneCatalog newCatalog = XMLLoader.getObjectFromStream(new URI("dummy"), new ByteArrayInputStream(newCatalogStr.getBytes(Charset.forName("UTF-8"))), StandaloneCatalog.class);
+
+ final DefaultPlan targetPlan = newCatalog.findCurrentPlan("something-with-fixed-term");
+ Assert.assertEquals(targetPlan.getInitialPhases().length, 2);
+ Assert.assertEquals(targetPlan.getInitialPhases()[1].getPhaseType(), PhaseType.FIXEDTERM);
+
+ }
+
+
+
+ @Test(groups = "fast")
public void testVerifyXML() throws Exception {
final StandaloneCatalog originalCatalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class);
@@ -341,6 +395,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
" <plans>\n" +
" <plan name=\"dynamic-annual\" prettyName=\"dynamic-annual\">\n" +
" <product>Dynamic</product>\n" +
+ " <recurringBillingMode>IN_ADVANCE</recurringBillingMode>\n" +
" <initialPhases>\n" +
" <phase type=\"TRIAL\">\n" +
" <duration>\n" +
@@ -378,6 +433,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
" </plan>\n" +
" <plan name=\"sports-monthly\" prettyName=\"sports-monthly\">\n" +
" <product>Sports</product>\n" +
+ " <recurringBillingMode>IN_ADVANCE</recurringBillingMode>\n" +
" <initialPhases>\n" +
" <phase type=\"TRIAL\">\n" +
" <duration>\n" +
@@ -414,6 +470,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
" </plan>\n" +
" <plan name=\"standard-monthly\" prettyName=\"standard-monthly\">\n" +
" <product>Standard</product>\n" +
+ " <recurringBillingMode>IN_ADVANCE</recurringBillingMode>\n" +
" <initialPhases>\n" +
" <phase type=\"TRIAL\">\n" +
" <duration>\n" +
@@ -450,6 +507,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
" </plan>\n" +
" <plan name=\"super-monthly\" prettyName=\"super-monthly\">\n" +
" <product>Super</product>\n" +
+ " <recurringBillingMode>IN_ADVANCE</recurringBillingMode>\n" +
" <initialPhases>\n" +
" <phase type=\"TRIAL\">\n" +
" <duration>\n" +
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestVersionedCatalog.java b/catalog/src/test/java/org/killbill/billing/catalog/TestVersionedCatalog.java
index 3dcb60f..b70e54f 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestVersionedCatalog.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestVersionedCatalog.java
@@ -48,15 +48,9 @@ public class TestVersionedCatalog extends CatalogTestSuiteNoDB {
final DateTime dt214 = new DateTime("2011-02-14T00:01:00+00:00");
final DateTime dt3 = new DateTime("2011-03-03T00:01:00+00:00");
- // New subscription
- try {
- vc.findPlan("pistol-monthly", dt0, dt0);
- Assert.fail("Exception should have been thrown there are no plans for this date");
- } catch (CatalogApiException e) {
- // Expected behaviour
- log.error("Expected exception", e);
+ // We find it although the date provided is too early because we default to first catalog version
+ final Plan newSubPlan0 = vc.findPlan("pistol-monthly", dt0, dt0);
- }
final Plan newSubPlan1 = vc.findPlan("pistol-monthly", dt1, dt1);
final Plan newSubPlan2 = vc.findPlan("pistol-monthly", dt2, dt2);
final Plan newSubPlan214 = vc.findPlan("pistol-monthly", dt214, dt214);
catalog/src/test/resources/catalogTest.xml 48(+47 -1)
diff --git a/catalog/src/test/resources/catalogTest.xml b/catalog/src/test/resources/catalogTest.xml
index d15d55f..f8d9f93 100644
--- a/catalog/src/test/resources/catalogTest.xml
+++ b/catalog/src/test/resources/catalogTest.xml
@@ -143,6 +143,11 @@
<alignment>CHANGE_OF_PRICELIST</alignment>
</changeAlignmentCase>
<changeAlignmentCase>
+ <fromPriceList>notrial</fromPriceList>
+ <toPriceList>trial</toPriceList>
+ <alignment>CHANGE_OF_PLAN</alignment>
+ </changeAlignmentCase>
+ <changeAlignmentCase>
<alignment>START_OF_SUBSCRIPTION</alignment>
</changeAlignmentCase>
</changeAlignment>
@@ -297,6 +302,43 @@
</recurring>
</finalPhase>
</plan>
+ <plan name="blowdart-monthly-trial">
+ <product>Blowdart</product>
+ <initialPhases>
+ <phase type="TRIAL">
+ <duration>
+ <unit>DAYS</unit>
+ <number>30</number>
+ </duration>
+ <fixed>
+ <fixedPrice>
+ </fixedPrice>
+ </fixed>
+ </phase>
+ </initialPhases>
+ <finalPhase type="EVERGREEN">
+ <duration>
+ <unit>UNLIMITED</unit>
+ </duration>
+ <recurring>
+ <billingPeriod>MONTHLY</billingPeriod>
+ <recurringPrice>
+ <price>
+ <currency>USD</currency>
+ <value>29.95</value>
+ </price>
+ <price>
+ <currency>EUR</currency>
+ <value>29.95</value>
+ </price>
+ <price>
+ <currency>GBP</currency>
+ <value>29.95</value>
+ </price>
+ </recurringPrice>
+ </recurring>
+ </finalPhase>
+ </plan>
<plan name="pistol-weekly">
<product>Pistol</product>
<initialPhases>
@@ -1333,7 +1375,11 @@
<plans>
<plan>blowdart-monthly-notrial</plan>
<plan>pistol-monthly-notrial</plan>
-
+ </plans>
+ </childPriceList>
+ <childPriceList name="trial">
+ <plans>
+ <plan>blowdart-monthly-trial</plan>
</plans>
</childPriceList>
</priceLists>
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
index f460e16..345da81 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
@@ -37,6 +37,7 @@ import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.Product;
@@ -540,7 +541,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
@Override
- public Entitlement changePlan(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
+ public Entitlement changePlan(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
logChangePlan(log, this, spec, overrides, null, null);
@@ -611,7 +612,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
}
@Override
- public Entitlement changePlanWithDate(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, @Nullable final LocalDate effectiveDate, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
+ public Entitlement changePlanWithDate(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, @Nullable final LocalDate effectiveDate, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
logChangePlan(log, this, spec, overrides, effectiveDate, null);
@@ -688,7 +689,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
}
@Override
- public Entitlement changePlanOverrideBillingPolicy(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final LocalDate unused, final BillingActionPolicy actionPolicy, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
+ public Entitlement changePlanOverrideBillingPolicy(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final LocalDate unused, final BillingActionPolicy actionPolicy, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
logChangePlan(log, this, spec, overrides, null, actionPolicy);
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
index eb2a68d..03811c9 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
@@ -65,7 +65,7 @@ public class DefaultEntitlementContext implements EntitlementContext {
private static <T> Iterable<T> merge(final Iterable<T> prevValues, final Iterable<T> newValues) {
// Be lenient if a plugin returns an empty list (default behavior for Ruby plugins): at this point,
// we know the isAborted flag hasn't been set, so let's assume the user actually wants to use the previous list
- if (newValues != null && !Iterables.<BaseEntitlementWithAddOnsSpecifier>isEmpty(newValues)) {
+ if (newValues != null && !Iterables.isEmpty(newValues)) {
return newValues;
} else {
return prevValues;
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java
index 6771c42..f84a951 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java
@@ -264,7 +264,7 @@ public class TestDefaultEntitlement extends EntitlementTestSuiteWithEmbeddedDB {
// Immediate change during trial
testListener.pushExpectedEvent(NextEvent.CREATE);
- entitlement.changePlan(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), callContext);
+ entitlement.changePlan(new PlanPhaseSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), callContext);
assertListenerStatus();
// Verify the change is immediate
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionApi.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionApi.java
index 18ac1dd..c73dcdb 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionApi.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionApi.java
@@ -498,7 +498,7 @@ public class TestDefaultSubscriptionApi extends EntitlementTestSuiteWithEmbedded
assertListenerStatus();
try {
- entitlement.changePlan(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), callContext);
+ entitlement.changePlan(new PlanPhaseSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), callContext);
fail();
} catch (final EntitlementApiException e) {
assertEquals(e.getCode(), ErrorCode.BLOCK_BLOCKED_ACTION.getCode());
@@ -507,7 +507,7 @@ public class TestDefaultSubscriptionApi extends EntitlementTestSuiteWithEmbedded
}
try {
- entitlement.changePlanWithDate(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, clock.getUTCToday(), ImmutableList.<PluginProperty>of(), callContext);
+ entitlement.changePlanWithDate(new PlanPhaseSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, clock.getUTCToday(), ImmutableList.<PluginProperty>of(), callContext);
fail();
} catch (final EntitlementApiException e) {
assertEquals(e.getCode(), ErrorCode.BLOCK_BLOCKED_ACTION.getCode());
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/engine/core/TestEntitlementUtils.java b/entitlement/src/test/java/org/killbill/billing/entitlement/engine/core/TestEntitlementUtils.java
index 1a0ced3..fd0d5b7 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/engine/core/TestEntitlementUtils.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/engine/core/TestEntitlementUtils.java
@@ -232,7 +232,7 @@ public class TestEntitlementUtils extends EntitlementTestSuiteWithEmbeddedDB {
@Test(groups = "slow", description = "Verify add-ons blocking states are added for EOT change plans")
public void testChangePlanEOT() throws Exception {
// Change plan EOT to Assault-Rifle (Telescopic-Scope is included)
- final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlanWithDate(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, new LocalDate(2013, 10, 7), ImmutableList.<PluginProperty>of(), callContext);
+ final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlanWithDate(new PlanPhaseSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, new LocalDate(2013, 10, 7), ImmutableList.<PluginProperty>of(), callContext);
// No blocking event (EOT)
assertListenerStatus();
@@ -271,7 +271,7 @@ public class TestEntitlementUtils extends EntitlementTestSuiteWithEmbeddedDB {
assertListenerStatus();
// Change plan EOT to Assault-Rifle (Telescopic-Scope is included)
- final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlanWithDate(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, new LocalDate(2013, 10, 7), ImmutableList.<PluginProperty>of(), callContext);
+ final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlanWithDate(new PlanPhaseSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, new LocalDate(2013, 10, 7), ImmutableList.<PluginProperty>of(), callContext);
// No blocking event (EOT)
assertListenerStatus();
@@ -290,7 +290,7 @@ public class TestEntitlementUtils extends EntitlementTestSuiteWithEmbeddedDB {
// Change plan IMM (upgrade) to Assault-Rifle (Telescopic-Scope is included)
testListener.pushExpectedEvents(NextEvent.CHANGE, NextEvent.CANCEL, NextEvent.BLOCK);
- final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlan(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), callContext);
+ final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlan(new PlanPhaseSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), callContext);
assertListenerStatus();
// We need to add a 1s delay before invoking the eventsStreamBuilder in the checks below, because
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 e4217f3..3a0bcda 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
@@ -306,24 +306,35 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
} else {
if (existingInvoicesForExternalCharges.get(invoiceIdForExternalCharge) == null) {
final Invoice existingInvoiceForExternalCharge = getInvoice(invoiceIdForExternalCharge, context);
+ if (InvoiceStatus.COMMITTED.equals(existingInvoiceForExternalCharge.getStatus())) {
+ throw new InvoiceApiException(ErrorCode.INVOICE_ALREADY_COMMITTED, existingInvoiceForExternalCharge.getId());
+ }
existingInvoicesForExternalCharges.put(invoiceIdForExternalCharge, existingInvoiceForExternalCharge);
}
invoiceForExternalCharge = existingInvoicesForExternalCharges.get(invoiceIdForExternalCharge);
}
final LocalDate startDate = MoreObjects.firstNonNull(charge.getStartDate(), effectiveDate);
- final LocalDate endDate = MoreObjects.firstNonNull(charge.getEndDate(), effectiveDate);
+ final LocalDate endDate = charge.getEndDate();
final InvoiceItem externalCharge = new ExternalChargeInvoiceItem(UUIDs.randomUUID(),
context.getCreatedDate(),
invoiceForExternalCharge.getId(),
accountId,
charge.getBundleId(),
+ charge.getSubscriptionId(),
+ charge.getPlanName(),
+ charge.getPhaseName(),
+ charge.getPrettyPlanName(),
+ charge.getPrettyPhaseName(),
charge.getDescription(),
startDate,
endDate,
charge.getAmount(),
- charge.getCurrency());
+ charge.getRate(),
+ charge.getCurrency(),
+ charge.getLinkedItemId());
+
invoiceForExternalCharge.addInvoiceItem(externalCharge);
}
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 5537dee..f7802ed 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
@@ -276,6 +276,17 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
}
@Override
+ public InvoiceModelDao getEarliestDraftInvoiceByAccount(final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<InvoiceModelDao>() {
+ @Override
+ public InvoiceModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+ final InvoiceSqlDao invoiceDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
+ return invoiceDao.getEarliestDraftInvoiceByAccount(context);
+ }
+ });
+ }
+
+ @Override
public void createInvoice(final InvoiceModelDao invoice,
final FutureAccountNotifications callbackDateTimePerSubscriptions,
final InternalCallContext context) {
@@ -291,9 +302,14 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
private List<InvoiceItemModelDao> createInvoices(final Iterable<InvoiceModelDao> invoices,
final FutureAccountNotifications callbackDateTimePerSubscriptions,
final InternalCallContext context) {
+ // Track invoices that are being created
final Collection<UUID> createdInvoiceIds = new HashSet<UUID>();
- final Collection<UUID> modifiedInvoiceIds = new HashSet<UUID>();
- final Collection<UUID> committedInvoiceIds = new HashSet<UUID>();
+ // Track invoices that already exist but are being committed -- AUTO_INVOICING_REUSE_DRAFT mode
+ final Collection<UUID> committedReusedInvoiceId = new HashSet<UUID>();
+ // Track all invoices that are referenced through all invoiceItems
+ final Collection<UUID> allInvoiceIds = new HashSet<UUID>();
+ // Track invoices that are committed but were not created or reused -- to sent the InvoiceAdjustment bus event
+ final Collection<UUID> adjustedCommittedInvoiceIds = new HashSet<UUID>();
final Collection<UUID> invoiceIdsReferencedFromItems = new HashSet<UUID>();
for (final InvoiceModelDao invoiceModelDao : invoices) {
@@ -302,7 +318,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
}
}
- if (Iterables.<InvoiceModelDao>isEmpty(invoices)) {
+ if (Iterables.isEmpty(invoices)) {
return ImmutableList.<InvoiceItemModelDao>of();
}
final UUID accountId = invoices.iterator().next().getAccountId();
@@ -321,28 +337,36 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
invoiceByInvoiceId.put(invoiceModelDao.getId(), invoiceModelDao);
final boolean isNotShellInvoice = invoiceIdsReferencedFromItems.remove(invoiceModelDao.getId());
- // Create the invoice if this is not a shell invoice and it does not already exist
- if (isNotShellInvoice && invoiceSqlDao.getById(invoiceModelDao.getId().toString(), context) == null) {
- createAndRefresh(invoiceSqlDao, invoiceModelDao, context);
- createdInvoiceIds.add(invoiceModelDao.getId());
+ final InvoiceModelDao invoiceOnDisk = invoiceSqlDao.getById(invoiceModelDao.getId().toString(), context);
+ if (isNotShellInvoice) {
+ // Create the invoice if this is not a shell invoice and it does not already exist
+ if (invoiceOnDisk == null) {
+ createAndRefresh(invoiceSqlDao, invoiceModelDao, context);
+ createdInvoiceIds.add(invoiceModelDao.getId());
+ } else if (invoiceOnDisk.getStatus() == InvoiceStatus.DRAFT && invoiceModelDao.getStatus() == InvoiceStatus.COMMITTED) {
+ invoiceSqlDao.updateStatus(invoiceModelDao.getId().toString(), InvoiceStatus.COMMITTED.toString(), context);
+ committedReusedInvoiceId.add(invoiceModelDao.getId());
+ }
}
// Create the invoice items if needed (note: they may not necessarily belong to that invoice)
for (final InvoiceItemModelDao invoiceItemModelDao : invoiceModelDao.getInvoiceItems()) {
if (transInvoiceItemSqlDao.getById(invoiceItemModelDao.getId().toString(), context) == null) {
createdInvoiceItems.add(createInvoiceItemFromTransaction(transInvoiceItemSqlDao, invoiceItemModelDao, context));
- modifiedInvoiceIds.add(invoiceItemModelDao.getInvoiceId());
+ allInvoiceIds.add(invoiceItemModelDao.getInvoiceId());
}
}
- final boolean wasInvoiceCreated = createdInvoiceIds.contains(invoiceModelDao.getId());
+ final boolean wasInvoiceCreatedOrCommitted = createdInvoiceIds.contains(invoiceModelDao.getId()) ||
+ committedReusedInvoiceId.contains(invoiceModelDao.getId());
if (InvoiceStatus.COMMITTED.equals(invoiceModelDao.getStatus())) {
- committedInvoiceIds.add(invoiceModelDao.getId());
- if (wasInvoiceCreated) {
+ if (wasInvoiceCreatedOrCommitted) {
notifyBusOfInvoiceCreation(entitySqlDaoWrapperFactory, invoiceModelDao, context);
+ } else {
+ adjustedCommittedInvoiceIds.add(invoiceModelDao.getId());
}
- } else if (wasInvoiceCreated && invoiceModelDao.isParentInvoice()) {
+ } else if (wasInvoiceCreatedOrCommitted && invoiceModelDao.isParentInvoice()) {
// Commit queue
notifyOfParentInvoiceCreation(entitySqlDaoWrapperFactory, invoiceModelDao, context);
}
@@ -351,7 +375,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
notifyOfFutureBillingEvents(entitySqlDaoWrapperFactory, invoiceModelDao.getAccountId(), callbackDateTimePerSubscriptions, context);
}
- for (final UUID adjustedInvoiceId : modifiedInvoiceIds) {
+ for (final UUID adjustedInvoiceId : allInvoiceIds) {
final boolean newInvoice = createdInvoiceIds.contains(adjustedInvoiceId);
if (newInvoice) {
// New invoice, so no associated payment yet: no need to refresh the invoice state
@@ -362,7 +386,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
cbaDao.doCBAComplexityFromTransaction(adjustedInvoiceId, invoicesTags, entitySqlDaoWrapperFactory, context);
}
- if (committedInvoiceIds.contains(adjustedInvoiceId) && !newInvoice) {
+ if (adjustedCommittedInvoiceIds.contains(adjustedInvoiceId)) {
// Notify the bus since the balance of the invoice changed (only if the invoice is COMMITTED)
notifyBusOfInvoiceAdjustment(entitySqlDaoWrapperFactory, adjustedInvoiceId, accountId, context.getUserToken(), context);
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
index 377b150..3f12d60 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
@@ -48,6 +48,8 @@ public interface InvoiceDao extends EntityDao<InvoiceModelDao, Invoice, InvoiceA
public void setFutureAccountNotificationsForEmptyInvoice(final UUID accountId, final FutureAccountNotifications callbackDateTimePerSubscriptions,
final InternalCallContext context);
+ InvoiceModelDao getEarliestDraftInvoiceByAccount(InternalTenantContext context);
+
InvoiceModelDao getByNumber(Integer number, InternalTenantContext context) throws InvoiceApiException;
List<InvoiceModelDao> getInvoicesByAccount(InternalTenantContext context);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDaoHelper.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDaoHelper.java
index ecebd92..3343234 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDaoHelper.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDaoHelper.java
@@ -227,7 +227,7 @@ public class InvoiceDaoHelper {
}
public void populateChildren(final Iterable<InvoiceModelDao> invoices, final List<Tag> invoicesTags, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext context) {
- if (Iterables.<InvoiceModelDao>isEmpty(invoices)) {
+ if (Iterables.isEmpty(invoices)) {
return;
}
@@ -242,7 +242,7 @@ public class InvoiceDaoHelper {
return !invoice.isParentInvoice();
}
});
- if (!Iterables.<InvoiceModelDao>isEmpty(nonParentInvoices)) {
+ if (!Iterables.isEmpty(nonParentInvoices)) {
setParentInvoice(nonParentInvoices,
invoicesTags,
entitySqlDaoWrapperFactory,
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceSqlDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceSqlDao.java
index 6d405b0..3acaab5 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceSqlDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceSqlDao.java
@@ -59,5 +59,8 @@ public interface InvoiceSqlDao extends EntitySqlDao<InvoiceModelDao, Invoice> {
@SqlQuery
List<InvoiceModelDao> getByIds(@BindIn("ids") final Collection<String> invoiceIds,
@SmartBindBean final InternalTenantContext context);
+ @SqlQuery
+ InvoiceModelDao getEarliestDraftInvoiceByAccount(@SmartBindBean final InternalTenantContext context);
+
}
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 1d73b89..69bb538 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
@@ -66,10 +66,13 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
* adjusts target date to the maximum invoice target date, if future invoices exist
*/
@Override
- public InvoiceWithMetadata generateInvoice(final ImmutableAccountData account, @Nullable final BillingEventSet events,
+ public InvoiceWithMetadata generateInvoice(final ImmutableAccountData account,
+ @Nullable final BillingEventSet events,
@Nullable final List<Invoice> existingInvoices,
+ @Nullable final UUID targetInvoiceId,
final LocalDate targetDate,
- final Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException {
+ final Currency targetCurrency,
+ final InternalCallContext context) throws InvoiceApiException {
if ((events == null) || (events.size() == 0) || events.isAccountAutoInvoiceOff()) {
return new InvoiceWithMetadata(null, ImmutableMap.<UUID, SubscriptionFutureNotificationDates>of());
}
@@ -79,14 +82,16 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
final LocalDate invoiceDate = context.toLocalDate(context.getCreatedDate());
final InvoiceStatus invoiceStatus = events.isAccountAutoInvoiceDraft() ? InvoiceStatus.DRAFT : InvoiceStatus.COMMITTED;
- final DefaultInvoice invoice = new DefaultInvoice(account.getId(), invoiceDate, adjustedTargetDate, targetCurrency, invoiceStatus);
- final UUID invoiceId = invoice.getId();
+ final DefaultInvoice invoice = targetInvoiceId != null ?
+ new DefaultInvoice(targetInvoiceId, account.getId(), null, invoiceDate, adjustedTargetDate, targetCurrency, false, invoiceStatus) :
+ new DefaultInvoice(account.getId(), invoiceDate, adjustedTargetDate, targetCurrency, invoiceStatus);
+
final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates = new HashMap<UUID, SubscriptionFutureNotificationDates>();
- final List<InvoiceItem> fixedAndRecurringItems = recurringInvoiceItemGenerator.generateItems(account, invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates, context);
+ final List<InvoiceItem> fixedAndRecurringItems = recurringInvoiceItemGenerator.generateItems(account, invoice.getId(), events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates, context);
invoice.addInvoiceItems(fixedAndRecurringItems);
- final List<InvoiceItem> usageItems = usageInvoiceItemGenerator.generateItems(account, invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates, context);
+ final List<InvoiceItem> usageItems = usageInvoiceItemGenerator.generateItems(account, invoice.getId(), events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates, context);
invoice.addInvoiceItems(usageItems);
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
index 6a22b6a..6ff69c5 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
@@ -141,11 +141,11 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator
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<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, thisEvent, adjustedNextEvent, targetDate, currency, invoiceItemGeneratorLogger, events.getRecurringBillingMode(), perSubscriptionFutureNotificationDate, internalCallContext);
+ final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, thisEvent, adjustedNextEvent, targetDate, currency, invoiceItemGeneratorLogger, thisEvent.getPlan().getRecurringBillingMode(), perSubscriptionFutureNotificationDate, internalCallContext);
proposedItems.addAll(newProposedItems);
}
}
- final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, nextEvent, null, targetDate, currency, invoiceItemGeneratorLogger, events.getRecurringBillingMode(), perSubscriptionFutureNotificationDate, internalCallContext);
+ final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, nextEvent, null, targetDate, currency, invoiceItemGeneratorLogger, nextEvent.getPlan().getRecurringBillingMode(), perSubscriptionFutureNotificationDate, internalCallContext);
proposedItems.addAll(newProposedItems);
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 e513a00..fac2799 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,6 +17,7 @@
package org.killbill.billing.invoice.generator;
import java.util.List;
+import java.util.UUID;
import javax.annotation.Nullable;
@@ -30,6 +31,6 @@ import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.junction.BillingEventSet;
public interface InvoiceGenerator {
- InvoiceWithMetadata generateInvoice(ImmutableAccountData account, @Nullable BillingEventSet events, @Nullable List<Invoice> existingInvoices,
- LocalDate targetDate, Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException;
+ InvoiceWithMetadata generateInvoice(ImmutableAccountData account, @Nullable BillingEventSet events, @Nullable List<Invoice> existingInvoices,
+ final UUID targetInvoiceId, LocalDate targetDate, Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException;
}
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 1901d42..eabea2e 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
@@ -27,7 +27,6 @@ import org.killbill.billing.invoice.api.DefaultInvoiceService;
import org.killbill.billing.invoice.api.InvoiceApiHelper;
import org.killbill.billing.invoice.api.InvoiceInternalApi;
import org.killbill.billing.invoice.api.InvoiceListenerService;
-import org.killbill.billing.invoice.api.InvoiceNotifier;
import org.killbill.billing.invoice.api.InvoicePaymentApi;
import org.killbill.billing.invoice.api.InvoiceService;
import org.killbill.billing.invoice.api.InvoiceUserApi;
@@ -47,10 +46,8 @@ 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;
import org.killbill.billing.invoice.notification.NextBillingDateNotifier;
import org.killbill.billing.invoice.notification.NextBillingDatePoster;
-import org.killbill.billing.invoice.notification.NullInvoiceNotifier;
import org.killbill.billing.invoice.plugin.api.InvoicePluginApi;
import org.killbill.billing.invoice.template.bundles.DefaultResourceBundleFactory;
import org.killbill.billing.invoice.usage.RawUsageOptimizer;
@@ -121,14 +118,6 @@ public class DefaultInvoiceModule extends KillBillModule implements InvoiceModul
bind(InvoiceFormatterFactory.class).to(config.getInvoiceFormatterFactoryClass()).asEagerSingleton();
}
- protected void installInvoiceNotifier() {
- if (staticInvoiceConfig.isEmailNotificationsEnabled()) {
- bind(InvoiceNotifier.class).to(EmailInvoiceNotifier.class).asEagerSingleton();
- } else {
- bind(InvoiceNotifier.class).to(NullInvoiceNotifier.class).asEagerSingleton();
- }
- }
-
protected void installInvoiceDispatcher() {
bind(InvoiceDispatcher.class).asEagerSingleton();
}
@@ -157,7 +146,6 @@ public class DefaultInvoiceModule extends KillBillModule implements InvoiceModul
installInvoicePluginApi();
installInvoiceServices();
- installInvoiceNotifier();
installNotifiers();
installInvoiceDispatcher();
installInvoiceListener();
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 d8fc889..2d547ac 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -61,7 +61,6 @@ 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.api.InvoiceNotifier;
import org.killbill.billing.invoice.api.InvoiceStatus;
import org.killbill.billing.invoice.api.user.DefaultInvoiceNotificationInternalEvent;
import org.killbill.billing.invoice.api.user.DefaultNullInvoiceEvent;
@@ -134,7 +133,6 @@ public class InvoiceDispatcher {
private final SubscriptionBaseInternalApi subscriptionApi;
private final InvoiceDao invoiceDao;
private final InternalCallContextFactory internalCallContextFactory;
- private final InvoiceNotifier invoiceNotifier;
private final InvoicePluginDispatcher invoicePluginDispatcher;
private final GlobalLocker locker;
private final PersistentBus eventBus;
@@ -150,7 +148,6 @@ public class InvoiceDispatcher {
final SubscriptionBaseInternalApi SubscriptionApi,
final InvoiceDao invoiceDao,
final InternalCallContextFactory internalCallContextFactory,
- final InvoiceNotifier invoiceNotifier,
final InvoicePluginDispatcher invoicePluginDispatcher,
final GlobalLocker locker,
final PersistentBus eventBus,
@@ -164,7 +161,6 @@ public class InvoiceDispatcher {
this.accountApi = accountApi;
this.invoiceDao = invoiceDao;
this.internalCallContextFactory = internalCallContextFactory;
- this.invoiceNotifier = invoiceNotifier;
this.invoicePluginDispatcher = invoicePluginDispatcher;
this.locker = locker;
this.eventBus = eventBus;
@@ -210,7 +206,7 @@ public class InvoiceDispatcher {
return processAccountFromNotificationOrBusEvent(accountId, targetDate, dryRunArguments, context);
} catch (final SubscriptionBaseApiException e) {
log.warn("Failed handling SubscriptionBase change.",
- new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, subscriptionId.toString()));
+ new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, subscriptionId.toString()));
return null;
}
}
@@ -382,7 +378,16 @@ public class InvoiceDispatcher {
}));
final Currency targetCurrency = account.getCurrency();
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, billingEvents, invoices, targetDate, targetCurrency, context);
+
+ final UUID targetInvoiceId;
+ if (billingEvents.isAccountAutoInvoiceReuseDraft()) {
+ final InvoiceModelDao earliestDraftInvoice = invoiceDao.getEarliestDraftInvoiceByAccount(context);
+ targetInvoiceId = earliestDraftInvoice != null ? earliestDraftInvoice.getId() : null;
+ } else {
+ targetInvoiceId = null;
+ }
+
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, billingEvents, invoices, targetInvoiceId, targetDate, targetCurrency, context);
final DefaultInvoice invoice = invoiceWithMetadata.getInvoice();
// Compute future notifications
@@ -454,10 +459,6 @@ public class InvoiceDispatcher {
setChargedThroughDates(invoice.getInvoiceItems(FixedPriceInvoiceItem.class), invoice.getInvoiceItems(RecurringInvoiceItem.class), context);
- if (InvoiceStatus.COMMITTED.equals(invoice.getStatus())) {
- notifyAccountIfEnabled(account, invoice, isRealInvoiceWithNonEmptyItems, context);
- }
-
}
return invoice;
} catch (final AccountApiException e) {
@@ -513,12 +514,12 @@ public class InvoiceDispatcher {
private List<InvoiceItemModelDao> transformToInvoiceModelDao(final List<InvoiceItem> invoiceItems) {
return Lists.transform(invoiceItems,
- new Function<InvoiceItem, InvoiceItemModelDao>() {
- @Override
- public InvoiceItemModelDao apply(final InvoiceItem input) {
- return new InvoiceItemModelDao(input);
- }
- });
+ new Function<InvoiceItem, InvoiceItemModelDao>() {
+ @Override
+ public InvoiceItemModelDao apply(final InvoiceItem input) {
+ return new InvoiceItemModelDao(input);
+ }
+ });
}
private Set<UUID> getUniqueInvoiceIds(final Invoice invoice) {
@@ -561,18 +562,6 @@ public class InvoiceDispatcher {
return isThereAnyItemsLeft;
}
- 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(fullAccount, new DefaultInvoice(invoiceDao.getById(invoice.getId(), context)), buildTenantContext(context));
- }
- }
-
private InvoiceItem computeCBAOnExistingInvoice(final Invoice invoice, final InternalCallContext context) throws InvoiceApiException {
// Transformation to Invoice -> InvoiceModelDao
final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoice);
@@ -826,14 +815,17 @@ public class InvoiceDispatcher {
private boolean shouldIgnoreChildInvoice(final Invoice childInvoice, final BigDecimal childInvoiceAmount) {
switch (childInvoiceAmount.compareTo(BigDecimal.ZERO)) {
- case -1 :
+ case -1:
// do nothing if child invoice has negative amount because it's a credit and it will be use in next invoice
return true;
- case 1 : return false;
- case 0 :
+ case 1:
+ return false;
+ case 0:
// only ignore if amount == 0 and any item is not FIXED or RECURRING
for (InvoiceItem item : childInvoice.getInvoiceItems()) {
- if (item.getInvoiceItemType().equals(InvoiceItemType.FIXED) || item.getInvoiceItemType().equals(InvoiceItemType.RECURRING)) return false;
+ if (item.getInvoiceItemType().equals(InvoiceItemType.FIXED) || item.getInvoiceItemType().equals(InvoiceItemType.RECURRING)) {
+ return false;
+ }
}
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java
index 442a4f8..4dcd360 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java
@@ -283,6 +283,16 @@ public class DefaultInvoice extends EntityBase implements Invoice, Cloneable {
}
@Override
+ public UUID getParentAccountId() {
+ return parentInvoice != null ? parentInvoice.getAccountId() : null;
+ }
+
+ @Override
+ public UUID getParentInvoiceId() {
+ return parentInvoice != null ? parentInvoice.getId() : null;
+ }
+
+ @Override
public String toString() {
return "DefaultInvoice [items=" + invoiceItems + ", payments=" + payments + ", id=" + id + ", accountId=" + accountId
+ ", invoiceDate=" + invoiceDate + ", targetDate=" + targetDate + ", currency=" + currency + ", amountPaid=" + getPaidAmount()
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/ExternalChargeInvoiceItem.java b/invoice/src/main/java/org/killbill/billing/invoice/model/ExternalChargeInvoiceItem.java
index 7797018..e2dc4a5 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/ExternalChargeInvoiceItem.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/ExternalChargeInvoiceItem.java
@@ -27,7 +27,7 @@ import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.util.UUIDs;
-public class ExternalChargeInvoiceItem extends InvoiceItemBase {
+public class ExternalChargeInvoiceItem extends InvoiceItemCatalogBase {
public ExternalChargeInvoiceItem(final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId, @Nullable final String description,
final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency) {
@@ -41,7 +41,13 @@ public class ExternalChargeInvoiceItem extends InvoiceItemBase {
public ExternalChargeInvoiceItem(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId,
@Nullable final String description, final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency) {
- super(id, createdDate, invoiceId, accountId, bundleId, null, description, startDate, endDate, amount, null, currency, null);
+ super(id, createdDate, invoiceId, accountId, bundleId, null, description, null, null, null, startDate, endDate, amount, null, currency, null);
+ }
+
+ public ExternalChargeInvoiceItem(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, final UUID bundleId, final UUID subscriptionId,
+ final String planName, final String phaseName, final String prettyPlanName, final String prettyPhaseName, @Nullable final String description, final LocalDate startDate, final LocalDate endDate,
+ final BigDecimal amount, final BigDecimal rate, final Currency currency, @Nullable final UUID linkedItemId) {
+ super(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, description, planName, phaseName, null, prettyPlanName, prettyPhaseName, null, startDate, endDate, amount, rate, currency, linkedItemId);
}
@Override
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
index b9eda76..0e6ea00 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
@@ -206,6 +206,9 @@ public abstract class InvoiceItemBase extends EntityBase implements InvoiceItem
final InvoiceItemBase that = (InvoiceItemBase) o;
+ if (getInvoiceItemType() != null ? !getInvoiceItemType().equals(that.getInvoiceItemType()) : that.getInvoiceItemType() != null) {
+ return false;
+ }
if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
return false;
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemFactory.java b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemFactory.java
index 07b8974..e8eef20 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemFactory.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemFactory.java
@@ -80,7 +80,7 @@ public class InvoiceItemFactory {
final InvoiceItem item;
switch (type) {
case EXTERNAL_CHARGE:
- item = new ExternalChargeInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, description, startDate, endDate, amount, currency);
+ item = new ExternalChargeInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, prettyPlanName, prettyPlanPhaseName, description, startDate, endDate, amount, rate, currency, linkedItemId);
break;
case FIXED:
item = new FixedPriceInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, prettyPlanName, prettyPlanPhaseName, description, startDate, amount, currency);
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 44f2a30..f913ce3 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
@@ -347,6 +347,16 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
return invoice.isParentInvoice();
}
+ @Override
+ public UUID getParentAccountId() {
+ return invoice.getParentAccountId();
+ }
+
+ @Override
+ public UUID getParentInvoiceId() {
+ return invoice.getParentInvoiceId();
+ }
+
// Expose the fields for children classes. This is useful for further customization of the invoices
@SuppressWarnings("UnusedDeclaration")
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
index df593b8..d194140 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2014 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
*
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
@@ -16,11 +18,10 @@
package org.killbill.billing.invoice.tree;
+import java.math.BigDecimal;
import java.util.Comparator;
-import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
@@ -44,8 +45,8 @@ public class SubscriptionItemTree {
private final List<Item> items = new LinkedList<Item>();
private final List<Item> existingFullyAdjustedItems = new LinkedList<Item>();
- private final List<InvoiceItem> existingFixedItems = new LinkedList<InvoiceItem>();
- private final Map<LocalDate, InvoiceItem> remainingFixedItems = new HashMap<LocalDate, InvoiceItem>();
+ private final List<InvoiceItem> existingIgnoredItems = new LinkedList<InvoiceItem>();
+ private final List<InvoiceItem> remainingIgnoredItems = new LinkedList<InvoiceItem>();
private final List<InvoiceItem> pendingItemAdj = new LinkedList<InvoiceItem>();
private final UUID targetInvoiceId;
@@ -91,7 +92,12 @@ public class SubscriptionItemTree {
switch (invoiceItem.getInvoiceItemType()) {
case RECURRING:
- root.addExistingItem(new ItemsNodeInterval(root, new Item(invoiceItem, targetInvoiceId, ItemAction.ADD)));
+ if (invoiceItem.getAmount().compareTo(BigDecimal.ZERO) == 0) {
+ // Nothing to repair -- https://github.com/killbill/killbill/issues/783
+ existingIgnoredItems.add(invoiceItem);
+ } else {
+ root.addExistingItem(new ItemsNodeInterval(root, new Item(invoiceItem, targetInvoiceId, ItemAction.ADD)));
+ }
break;
case REPAIR_ADJ:
@@ -99,7 +105,7 @@ public class SubscriptionItemTree {
break;
case FIXED:
- existingFixedItems.add(invoiceItem);
+ existingIgnoredItems.add(invoiceItem);
break;
case ITEM_ADJ:
@@ -158,6 +164,17 @@ public class SubscriptionItemTree {
public void mergeProposedItem(final InvoiceItem invoiceItem) {
Preconditions.checkState(!isBuilt, "Tree already built, unable to add new invoiceItem=%s", invoiceItem);
+ // Check if it was an existing item ignored for tree purposes (e.g. FIXED or $0 RECURRING, both of which aren't repaired)
+ final InvoiceItem existingItem = Iterables.tryFind(existingIgnoredItems, new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ return input.matches(invoiceItem);
+ }
+ }).orNull();
+ if (existingItem != null) {
+ return;
+ }
+
switch (invoiceItem.getInvoiceItemType()) {
case RECURRING:
// merged means we've either matched the proposed to an existing, or triggered a repair
@@ -168,15 +185,7 @@ public class SubscriptionItemTree {
break;
case FIXED:
- final InvoiceItem existingItem = Iterables.tryFind(existingFixedItems, new Predicate<InvoiceItem>() {
- @Override
- public boolean apply(final InvoiceItem input) {
- return input.matches(invoiceItem);
- }
- }).orNull();
- if (existingItem == null) {
- remainingFixedItems.put(invoiceItem.getStartDate(), invoiceItem);
- }
+ remainingIgnoredItems.add(invoiceItem);
break;
default:
@@ -204,7 +213,7 @@ public class SubscriptionItemTree {
public List<InvoiceItem> getView() {
final List<InvoiceItem> tmp = new LinkedList<InvoiceItem>();
- tmp.addAll(remainingFixedItems.values());
+ tmp.addAll(remainingIgnoredItems);
tmp.addAll(Collections2.filter(Collections2.transform(items, new Function<Item, InvoiceItem>() {
@Override
public InvoiceItem apply(final Item input) {
@@ -278,8 +287,8 @@ public class SubscriptionItemTree {
sb.append(", isMerged=").append(isMerged);
sb.append(", items=").append(items);
sb.append(", existingFullyAdjustedItems=").append(existingFullyAdjustedItems);
- sb.append(", existingFixedItems=").append(existingFixedItems);
- sb.append(", remainingFixedItems=").append(remainingFixedItems);
+ sb.append(", existingIgnoredItems=").append(existingIgnoredItems);
+ sb.append(", remainingIgnoredItems=").append(remainingIgnoredItems);
sb.append(", pendingItemAdj=").append(pendingItemAdj);
sb.append('}');
return sb.toString();
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceSqlDao.sql.stg b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceSqlDao.sql.stg
index ab962d9..141dca0 100644
--- a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceSqlDao.sql.stg
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceSqlDao.sql.stg
@@ -71,6 +71,17 @@ getParentDraftInvoice() ::= <<
<defaultOrderBy("")>
>>
+getEarliestDraftInvoiceByAccount() ::= <<
+ SELECT <allTableFields("")>
+ FROM <tableName()>
+ WHERE <accountRecordIdField("")> = :accountRecordId
+ AND status = 'DRAFT'
+ <AND_CHECK_TENANT("")>
+ <defaultOrderBy("")>
+ limit 1
+;
+>>
+
getByIds(ids) ::= <<
select
<allTableFields("t.")>
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
index be8c47a..52b0e08 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
@@ -27,6 +27,7 @@ import javax.annotation.Nullable;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.callcontext.DefaultCallContext;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
@@ -35,6 +36,7 @@ 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.api.InvoiceStatus;
+import org.killbill.billing.invoice.api.InvoiceUserApi;
import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
import org.killbill.billing.util.api.TagApiException;
import org.killbill.billing.util.callcontext.CallContext;
@@ -111,64 +113,30 @@ public class TestDefaultInvoiceUserApi extends InvoiceTestSuiteWithEmbeddedDB {
Assert.assertEquals(adjustedAccountBalance, initialAccountBalance.add(externalChargeAmount));
}
- @Test(groups = "slow")
- public void testPostExternalChargeOnExistingInvoice() throws Exception {
- // Verify the initial invoice balance
- final BigDecimal invoiceBalance = invoiceUserApi.getInvoice(invoiceId, callContext).getBalance();
- Assert.assertEquals(invoiceBalance.compareTo(BigDecimal.ZERO), 1);
-
- // Verify the initial account balance
- final BigDecimal accountBalance = invoiceUserApi.getAccountBalance(accountId, callContext);
- Assert.assertEquals(accountBalance, invoiceBalance);
- // Post an external charge
- final BigDecimal externalChargeAmount = BigDecimal.TEN;
- final InvoiceItem externalCharge = new ExternalChargeInvoiceItem(invoiceId, accountId, null, UUID.randomUUID().toString(), clock.getUTCToday(), clock.getUTCToday(), externalChargeAmount, accountCurrency);
- final InvoiceItem externalChargeInvoiceItem = invoiceUserApi.insertExternalCharges(accountId, clock.getUTCToday(), ImmutableList.<InvoiceItem>of(externalCharge), true, callContext).get(0);
- verifyExternalChargeOnExistingInvoice(invoiceBalance, null, externalChargeAmount, externalChargeInvoiceItem);
- }
@Test(groups = "slow")
public void testOriginalAmountCharged() throws Exception {
- final Invoice initialInvoice = invoiceUserApi.getInvoice(invoiceId, callContext);
- final BigDecimal originalAmountCharged = initialInvoice.getOriginalChargedAmount();
- final BigDecimal amountCharged = initialInvoice.getChargedAmount();
- Assert.assertEquals(originalAmountCharged.compareTo(amountCharged), 0);
-
- ((ClockMock) clock).addDays(1);
-
- // Sleep at least one sec to make sure created_date for the external charge is different than the created date for the invoice itself
- CallContext newCallContextLater = new DefaultCallContext(accountId, callContext.getTenantId(), callContext.getUserName(), callContext.getCallOrigin(), callContext.getUserType(), callContext.getUserToken(), clock);
// Post an external charge
final BigDecimal externalChargeAmount = BigDecimal.TEN;
- final InvoiceItem externalCharge = new ExternalChargeInvoiceItem(invoiceId, accountId, null, UUID.randomUUID().toString(), clock.getUTCToday(), clock.getUTCToday(), externalChargeAmount, accountCurrency);
- final InvoiceItem externalChargeInvoiceItem = invoiceUserApi.insertExternalCharges(accountId, clock.getUTCToday(), ImmutableList.<InvoiceItem>of(externalCharge), true, newCallContextLater).get(0);
+ final InvoiceItem externalCharge = new ExternalChargeInvoiceItem(null, accountId, null, UUID.randomUUID().toString(), clock.getUTCToday(), null, externalChargeAmount, accountCurrency);
+ final InvoiceItem externalChargeInvoiceItem = invoiceUserApi.insertExternalCharges(accountId, clock.getUTCToday(), ImmutableList.<InvoiceItem>of(externalCharge), true, callContext).get(0);
- final Invoice newInvoice = invoiceUserApi.getInvoice(invoiceId, callContext);
- final BigDecimal newOriginalAmountCharged = newInvoice.getOriginalChargedAmount();
+ final Invoice newInvoice = invoiceUserApi.getInvoice(externalChargeInvoiceItem.getInvoiceId(), callContext);
final BigDecimal newAmountCharged = newInvoice.getChargedAmount();
- final BigDecimal expectedChargedAmount = newInvoice.getOriginalChargedAmount().add(externalChargeInvoiceItem.getAmount());
-
- Assert.assertEquals(originalAmountCharged.compareTo(newOriginalAmountCharged), 0);
- Assert.assertEquals(newAmountCharged.compareTo(expectedChargedAmount), 0);
+ Assert.assertEquals(newInvoice.getOriginalChargedAmount().compareTo(externalChargeAmount), 0);
+ Assert.assertEquals(newAmountCharged.compareTo(externalChargeAmount), 0);
}
@Test(groups = "slow")
- public void testPostExternalChargeForBundleOnExistingInvoice() throws Exception {
- // Verify the initial invoice balance
- final BigDecimal invoiceBalance = invoiceUserApi.getInvoice(invoiceId, callContext).getBalance();
- Assert.assertEquals(invoiceBalance.compareTo(BigDecimal.ZERO), 1);
-
- // Verify the initial account balance
- final BigDecimal accountBalance = invoiceUserApi.getAccountBalance(accountId, callContext);
- Assert.assertEquals(accountBalance, invoiceBalance);
+ public void testPostExternalChargeForBundle() throws Exception {
// Post an external charge
final BigDecimal externalChargeAmount = BigDecimal.TEN;
final UUID bundleId = UUID.randomUUID();
- final InvoiceItem externalCharge = new ExternalChargeInvoiceItem(invoiceId, accountId, bundleId, UUID.randomUUID().toString(), clock.getUTCToday(), clock.getUTCToday(), externalChargeAmount, accountCurrency);
+ final InvoiceItem externalCharge = new ExternalChargeInvoiceItem(null, accountId, bundleId, UUID.randomUUID().toString(), clock.getUTCToday(), null, externalChargeAmount, accountCurrency);
final InvoiceItem externalChargeInvoiceItem = invoiceUserApi.insertExternalCharges(accountId, clock.getUTCToday(), ImmutableList.<InvoiceItem>of(externalCharge), true, callContext).get(0);
- verifyExternalChargeOnExistingInvoice(invoiceBalance, bundleId, externalChargeAmount, externalChargeInvoiceItem);
+ Assert.assertEquals(externalChargeInvoiceItem.getBundleId(), bundleId);
}
private void verifyExternalChargeOnExistingInvoice(final BigDecimal initialInvoiceBalance, @Nullable final UUID bundleId,
@@ -373,5 +341,20 @@ public class TestDefaultInvoiceUserApi extends InvoiceTestSuiteWithEmbeddedDB {
creditInvoice = invoiceUserApi.getInvoice(invoiceId, callContext);
Assert.assertEquals(creditInvoice.getStatus(), InvoiceStatus.COMMITTED);
+ try {
+ final InvoiceItem externalCharge = new ExternalChargeInvoiceItem(invoiceId, accountId, null, "Initial external charge", clock.getUTCToday(), null, new BigDecimal("12.33"), accountCurrency);
+ invoiceUserApi.insertExternalCharges(accountId, clock.getUTCToday(), ImmutableList.of(externalCharge), true, callContext);
+ Assert.fail("Should fail to add external charge on already committed invoice");
+ } catch (final InvoiceApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.INVOICE_ALREADY_COMMITTED.getCode());
+ }
+
+ try {
+ invoiceUserApi.insertCreditForInvoice(accountId, invoiceId, creditAmount,
+ clock.getUTCToday(), accountCurrency, null, callContext);
+ Assert.fail("Should fail to add credit on already committed invoice");
+ } catch (final InvoiceApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.INVOICE_ALREADY_COMMITTED.getCode());
+ }
}
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestInvoiceFlagBehaviors.java b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestInvoiceFlagBehaviors.java
index a12af65..2810693 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestInvoiceFlagBehaviors.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestInvoiceFlagBehaviors.java
@@ -128,37 +128,6 @@ public class TestInvoiceFlagBehaviors extends InvoiceTestSuiteWithEmbeddedDB {
assertEquals(accountCBA2.compareTo(BigDecimal.ZERO), 0);
}
- @Test(groups = "slow", description = "Verify invoice/account balance with a WRITTEN_OFF invoice. Verify account credit is not added to a previously WRITTEN_OFF invoice." )
- public void testWrittenOffInvoiceWithAccountCredit2() throws Exception {
-
- // Add credit on the account
- invoiceUserApi.insertCredit(accountId, BigDecimal.TEN, null, accountCurrency, true, null, callContext);
-
- final BigDecimal accountBalance0 = invoiceUserApi.getAccountBalance(accountId, callContext);
- assertEquals(accountBalance0.compareTo(new BigDecimal("-10.0")), 0);
-
- final BigDecimal accountCBA0 = invoiceUserApi.getAccountCBA(accountId, callContext);
- assertEquals(accountCBA0.compareTo(BigDecimal.TEN), 0);
-
- // Create new invoice with one charge and expect account credit to be used
- final List<InvoiceItem> items = invoiceUserApi.insertExternalCharges(accountId, clock.getUTCToday(), ImmutableList.<InvoiceItem>of(new ExternalChargeInvoiceItem(UUID.randomUUID(), clock.getUTCNow(), null, accountId, null, null, null, null, new BigDecimal("4.0"), accountCurrency)), true, callContext);
- assertEquals(items.size(), 1);
-
- // Tag invoice with WRITTEN_OFF
- final UUID invoiceId = items.get(0).getInvoiceId();
- tagUserApi.addTag(invoiceId, ObjectType.INVOICE, ControlTagType.WRITTEN_OFF.getId(), callContext);
-
- // Add another charge on the **same invoice** => Because it is WRITTEN_OFF, we expect the CBA logic to not apply any credit
- final List<InvoiceItem> items2 = invoiceUserApi.insertExternalCharges(accountId, clock.getUTCToday(), ImmutableList.<InvoiceItem>of(new ExternalChargeInvoiceItem(UUID.randomUUID(), clock.getUTCNow(), invoiceId, accountId, null, null, null, null, new BigDecimal("10.0"), accountCurrency)), true, callContext);
- assertEquals(items2.size(), 1);
-
- final BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(accountId, callContext);
- assertEquals(accountBalance2.compareTo(new BigDecimal("-6.00")), 0);
-
- final BigDecimal accountCBA2 = invoiceUserApi.getAccountCBA(accountId, callContext);
- assertEquals(accountCBA2.compareTo(new BigDecimal("6.00")), 0);
- }
-
@Test(groups = "slow", description = "Verify invoice/account balance with migrated invoice. Verify account credit is not consumed and that invoice/account balance does not take into account migrated invoice.")
public void testMigratedInvoiceWithAccountCredit() throws Exception {
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
index 0e1ea27..3cf8c32 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
@@ -81,6 +81,11 @@ public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice,
}
@Override
+ public InvoiceModelDao getEarliestDraftInvoiceByAccount(final InternalTenantContext context) {
+ return null;
+ }
+
+ @Override
public List<InvoiceItemModelDao> createInvoices(final List<InvoiceModelDao> invoiceModelDaos, final InternalCallContext context) {
synchronized (monitor) {
final List<InvoiceItemModelDao> createdItems = new LinkedList<InvoiceItemModelDao>();
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 7199e5d..9f4ae9d 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
@@ -502,6 +502,42 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
+ public void testGetEarliestDraftInvoiceByAccount() throws EntityPersistenceException {
+ final UUID accountId = account.getId();
+
+ InvoiceModelDao result;
+ result = invoiceDao.getEarliestDraftInvoiceByAccount(context);
+ assertNull(result);
+
+ final LocalDate targetDate1 = new LocalDate(2011, 10, 6);
+ final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD, InvoiceStatus.DRAFT);
+ invoiceUtil.createInvoice(invoice1, context);
+
+ result = invoiceDao.getEarliestDraftInvoiceByAccount(context);
+ assertNotNull(result);
+ assertEquals(result.getId(), invoice1.getId());
+
+ final LocalDate targetDate2 = new LocalDate(2011, 12, 6);
+ final Invoice invoice2 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate2, Currency.USD);
+ invoiceUtil.createInvoice(invoice2, context);
+
+ result = invoiceDao.getEarliestDraftInvoiceByAccount(context);
+ assertNotNull(result);
+ assertEquals(result.getId(), invoice1.getId());
+
+
+ final LocalDate targetDate3 = new LocalDate(2011, 10, 6);
+ final Invoice invoice3 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate3, Currency.USD, InvoiceStatus.DRAFT);
+ invoiceUtil.createInvoice(invoice3, context);
+
+ result = invoiceDao.getEarliestDraftInvoiceByAccount(context);
+ assertNotNull(result);
+ assertEquals(result.getId(), invoice1.getId());
+ }
+
+
+
+ @Test(groups = "slow")
public void testAccountBalance() throws EntityPersistenceException {
final UUID accountId = account.getId();
final UUID bundleId = UUID.randomUUID();
@@ -1207,7 +1243,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
final BillingEventSet events = new MockBillingEventSet();
events.add(event1);
- final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, context);
+ final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, invoiceList, null, targetDate, Currency.USD, context);
final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
assertEquals(invoice1.getBalance(), KillBillMoney.of(TEN, invoice1.getCurrency()));
invoiceList.add(invoice1);
@@ -1226,7 +1262,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
// 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 InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, context);
+ final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, null, targetDate, Currency.USD, context);
final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
assertEquals(invoice2.getBalance(), KillBillMoney.of(FIVE, invoice2.getCurrency()));
@@ -1260,7 +1296,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
events.add(event);
final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 15);
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, context);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, context);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
@@ -1302,7 +1338,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
events.add(event1);
final UUID accountId = account.getId();
- final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, new LocalDate(effectiveDate1), Currency.USD, context);
+ final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, null, new LocalDate(effectiveDate1), Currency.USD, context);
final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
assertNotNull(invoice1);
assertEquals(invoice1.getNumberOfItems(), 1);
@@ -1319,7 +1355,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
"testEvent2", 2L, SubscriptionBaseTransitionType.PHASE);
events.add(event2);
- final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, new LocalDate(effectiveDate2), Currency.USD, context);
+ final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, null, new LocalDate(effectiveDate2), Currency.USD, context);
final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
assertNotNull(invoice2);
assertEquals(invoice2.getNumberOfItems(), 1);
@@ -1330,7 +1366,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
//invoiceUtil.createInvoice(invoice2, invoice2.getTargetDate().getDayOfMonth(), callcontext);
final DateTime effectiveDate3 = effectiveDate2.plusMonths(1);
- final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, events, invoiceList, new LocalDate(effectiveDate3), Currency.USD, context);
+ final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, events, invoiceList, null, new LocalDate(effectiveDate3), Currency.USD, context);
final Invoice invoice3 = invoiceWithMetadata3.getInvoice();
assertNotNull(invoice3);
assertEquals(invoice3.getNumberOfItems(), 1);
@@ -1342,7 +1378,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
@Test(groups = "slow")
public void testInvoiceForEmptyEventSet() throws InvoiceApiException {
final BillingEventSet events = new MockBillingEventSet();
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, new LocalDate(), Currency.USD, context);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, new LocalDate(), Currency.USD, context);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNull(invoice);
}
@@ -1377,7 +1413,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
"testEvent2", 2L, SubscriptionBaseTransitionType.CHANGE);
events.add(event2);
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, new LocalDate(effectiveDate2), Currency.USD, context);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, new LocalDate(effectiveDate2), Currency.USD, context);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
assertEquals(invoice.getNumberOfItems(), 2);
@@ -1440,6 +1476,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
final Plan plan = Mockito.mock(Plan.class);
Mockito.when(plan.getName()).thenReturn("plan");
+ Mockito.when(plan.getRecurringBillingMode()).thenReturn(BillingMode.IN_ADVANCE);
final PlanPhase phase1 = Mockito.mock(PlanPhase.class);
Mockito.when(phase1.getName()).thenReturn("plan-phase1");
@@ -1449,7 +1486,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
BillingPeriod.MONTHLY, 31, BillingMode.IN_ADVANCE,
"new-event", 1L, SubscriptionBaseTransitionType.CREATE);
events.add(event1);
- final InvoiceWithMetadata newInvoiceWithMetadata = generator.generateInvoice(account, events, invoices, targetDate, Currency.USD, context);
+ final InvoiceWithMetadata newInvoiceWithMetadata = generator.generateInvoice(account, events, invoices, null, targetDate, Currency.USD, context);
final Invoice newInvoice = newInvoiceWithMetadata.getInvoice();
invoiceUtil.createInvoice(newInvoice, context);
@@ -1469,6 +1506,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
final Plan plan = Mockito.mock(Plan.class);
Mockito.when(plan.getName()).thenReturn("plan");
+ Mockito.when(plan.getRecurringBillingMode()).thenReturn(BillingMode.IN_ADVANCE);
final PlanPhase phase1 = Mockito.mock(PlanPhase.class);
Mockito.when(phase1.getName()).thenReturn("plan-phase1");
@@ -1485,7 +1523,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
"testEvent1", 1L, SubscriptionBaseTransitionType.CHANGE);
events.add(event1);
- InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, invoices, new LocalDate(targetDate1), Currency.USD, context);
+ InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, invoices, null, new LocalDate(targetDate1), Currency.USD, context);
Invoice invoice1 = invoiceWithMetadata1.getInvoice();
invoices.add(invoice1);
invoiceUtil.createInvoice(invoice1, context);
@@ -1497,7 +1535,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
BillingPeriod.MONTHLY, 31, BillingMode.IN_ADVANCE,
"testEvent2", 2L, SubscriptionBaseTransitionType.CHANGE);
events.add(event2);
- InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoices, new LocalDate(targetDate2), Currency.USD, context);
+ InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoices, null, new LocalDate(targetDate2), Currency.USD, context);
Invoice invoice2 = invoiceWithMetadata2.getInvoice();
invoiceUtil.createInvoice(invoice2, context);
invoice2 = new DefaultInvoice(invoiceDao.getById(invoice2.getId(), context));
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 bfc6eb7..45f8c46 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
@@ -127,14 +127,14 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
@Test(groups = "fast")
public void testWithNullEventSetAndNullInvoiceSet() throws InvoiceApiException {
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, null, null, clock.getUTCToday(), Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, null, null, null, clock.getUTCToday(), Currency.USD, internalCallContext);
assertNull(invoiceWithMetadata.getInvoice());
}
@Test(groups = "fast")
public void testWithEmptyEventSet() throws InvoiceApiException {
final BillingEventSet events = new MockBillingEventSet();
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, clock.getUTCToday(), Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, clock.getUTCToday(), Currency.USD, internalCallContext);
assertNull(invoiceWithMetadata.getInvoice());
}
@@ -153,7 +153,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(event);
final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3);
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
assertEquals(invoice.getNumberOfItems(), 2);
@@ -184,7 +184,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(event);
final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3);
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
assertEquals(invoice.getNumberOfItems(), 2);
@@ -217,7 +217,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
// Target date is the next BCD, in local time
final LocalDate targetDate = invoiceUtil.buildDate(2012, 8, bcdLocal);
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
assertEquals(invoice.getNumberOfItems(), 2);
@@ -241,7 +241,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
// Set a target date of today (start date)
final LocalDate targetDate = startDate;
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
assertEquals(invoice.getNumberOfItems(), 1);
@@ -263,7 +263,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(event);
final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3);
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
assertEquals(invoice.getNumberOfItems(), 2);
@@ -295,7 +295,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(event2);
final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3);
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
assertEquals(invoice.getNumberOfItems(), 2);
@@ -321,7 +321,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final LocalDate targetDate = invoiceUtil.buildDate(2011, 12, 3);
final UUID accountId = UUID.randomUUID();
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
assertEquals(invoice.getNumberOfItems(), 4);
@@ -362,7 +362,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(event3);
final LocalDate targetDate = invoiceUtil.buildDate(2011, 12, 3);
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
assertEquals(invoice.getNumberOfItems(), 4);
@@ -384,13 +384,13 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(event1);
LocalDate targetDate = invoiceUtil.buildDate(2011, 12, 1);
- final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
final List<Invoice> existingInvoices = new ArrayList<Invoice>();
final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
existingInvoices.add(invoice1);
targetDate = invoiceUtil.buildDate(2011, 12, 3);
- final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, existingInvoices, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, existingInvoices, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
assertNull(invoice2);
}
@@ -563,7 +563,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 1);
events.add(createBillingEvent(UUID.randomUUID(), UUID.randomUUID(), targetDate, plan, planPhase, 1));
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
assertEquals(invoice.getInvoiceItems().size(), 1);
@@ -580,7 +580,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(createBillingEvent(UUID.randomUUID(), UUID.randomUUID(), startDate, plan, planPhase, startDate.getDayOfMonth()));
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
final RecurringInvoiceItem item = (RecurringInvoiceItem) invoice.getInvoiceItems().get(0);
@@ -618,14 +618,14 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(event2);
events.add(event1);
- final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, new LocalDate("2012-02-01"), Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, null, new LocalDate("2012-02-01"), Currency.USD, internalCallContext);
final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
assertNotNull(invoice1);
assertEquals(invoice1.getNumberOfItems(), 1);
final List<Invoice> invoiceList = new ArrayList<Invoice>();
invoiceList.add(invoice1);
- final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, new LocalDate("2012-04-05"), Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, null, new LocalDate("2012-04-05"), Currency.USD, internalCallContext);
final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
assertNotNull(invoice2);
assertEquals(invoice2.getNumberOfItems(), 1);
@@ -650,7 +650,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(event1);
// ensure both components are invoiced
- final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, startDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, null, startDate, Currency.USD, internalCallContext);
final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
assertNotNull(invoice1);
assertEquals(invoice1.getNumberOfItems(), 2);
@@ -663,7 +663,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final LocalDate currentDate = startDate.plusMonths(1);
// ensure that only the recurring price is invoiced
- final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, currentDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, null, currentDate, Currency.USD, internalCallContext);
final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
assertNotNull(invoice2);
assertEquals(invoice2.getNumberOfItems(), 1);
@@ -689,7 +689,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(event1);
// ensure that a single invoice item is generated for the fixed cost
- final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, startDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, null, startDate, Currency.USD, internalCallContext);
final Invoice invoice1 = invoiceWithMetadata1.getInvoice();assertNotNull(invoice1);
assertEquals(invoice1.getNumberOfItems(), 1);
assertEquals(invoice1.getBalance(), KillBillMoney.of(fixedCost1, invoice1.getCurrency()));
@@ -703,7 +703,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(event2);
// ensure that a single invoice item is generated for the fixed cost
- final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, phaseChangeDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, null, phaseChangeDate, Currency.USD, internalCallContext);
final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
assertEquals(invoice2.getNumberOfItems(), 1);
assertEquals(invoice2.getBalance(), KillBillMoney.of(fixedCost2, invoice2.getCurrency()));
@@ -735,7 +735,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final LocalDate discountPhaseEndDate = trialPhaseEndDate.plusMonths(6);
events.add(createBillingEvent(subscriptionId, bundleId, discountPhaseEndDate, plan1, phase3, BILL_CYCLE_DAY));
- final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, creationDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, null, creationDate, Currency.USD, internalCallContext);
final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
assertNotNull(invoice1);
assertEquals(invoice1.getNumberOfItems(), 1);
@@ -744,7 +744,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final List<Invoice> invoiceList = new ArrayList<Invoice>();
invoiceList.add(invoice1);
- final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, trialPhaseEndDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, null, trialPhaseEndDate, Currency.USD, internalCallContext);
final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
assertNotNull(invoice2);
assertEquals(invoice2.getNumberOfItems(), 1);
@@ -753,7 +753,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
invoiceList.add(invoice2);
LocalDate targetDate = new LocalDate(trialPhaseEndDate.getYear(), trialPhaseEndDate.getMonthOfYear(), BILL_CYCLE_DAY);
- final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, events, invoiceList, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice3 = invoiceWithMetadata3.getInvoice();
assertNotNull(invoice3);
assertEquals(invoice3.getNumberOfItems(), 1);
@@ -762,7 +762,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
invoiceList.add(invoice3);
targetDate = targetDate.plusMonths(6);
- final InvoiceWithMetadata invoiceWithMetadata4 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata4 = generator.generateInvoice(account, events, invoiceList, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice4 = invoiceWithMetadata4.getInvoice();
assertNotNull(invoice4);
assertEquals(invoice4.getNumberOfItems(), 7);
@@ -775,7 +775,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final Plan plan1 = new MockPlan();
final PlanPhase phase1 = createMockMonthlyPlanPhase(null, ZERO, PhaseType.TRIAL);
events.add(createBillingEvent(UUID.randomUUID(), UUID.randomUUID(), clock.getUTCToday(), plan1, phase1, 1));
- generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
}
@Test(groups = "fast")
@@ -795,7 +795,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(createBillingEvent(baseSubscription.getId(), baseSubscription.getBundleId(), april25, basePlan, basePlanEvergreen, 25));
// generate invoice
- final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, april25, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, null, april25, Currency.USD, internalCallContext);
final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
assertNotNull(invoice1);
assertEquals(invoice1.getNumberOfItems(), 1);
@@ -817,7 +817,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(createBillingEvent(addOnSubscription2.getId(), baseSubscription.getBundleId(), april28, addOn2Plan, addOn2PlanPhaseEvergreen, 25));
// generate invoice
- final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoices, april28, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoices, null, april28, Currency.USD, internalCallContext);
final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
invoices.add(invoice2);
@@ -836,7 +836,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
// generate invoice
final LocalDate may1 = new LocalDate(2012, 5, 1);
- final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, newEvents, invoices, may1, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, newEvents, invoices, null, may1, Currency.USD, internalCallContext);
final Invoice invoice3 = invoiceWithMetadata3.getInvoice();
assertNotNull(invoice3);
assertEquals(invoice3.getNumberOfItems(), 3);
@@ -859,7 +859,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final BillingEventSet events = new MockBillingEventSet();
events.add(createBillingEvent(originalSubscription.getId(), originalSubscription.getBundleId(), april25, originalPlan, originalPlanEvergreen, 25));
- final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, april25, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, null, april25, Currency.USD, internalCallContext);
final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
printDetailInvoice(invoice1);
@@ -882,7 +882,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(createBillingEvent(newSubscription.getId(), originalSubscription.getBundleId(), april25, newPlan, newPlanEvergreen, 25));
// generate a new invoice
- final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoices, april25, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoices, null, april25, Currency.USD, internalCallContext);
final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
printDetailInvoice(invoice2);
@@ -947,7 +947,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
// Generate a new invoice
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, null, targetDate, currency, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertEquals(invoice.getNumberOfItems(), 7);
assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
@@ -982,7 +982,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
existingInvoices.add(invoice);
// Generate next invoice (no-op)
- final InvoiceWithMetadata newInvoiceWithMetdata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+ final InvoiceWithMetadata newInvoiceWithMetdata = generator.generateInvoice(account, events, existingInvoices, null, targetDate, currency, internalCallContext);
final Invoice newInvoice = newInvoiceWithMetdata.getInvoice();
assertNull(newInvoice);
}
@@ -1004,7 +1004,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3);
final UUID accountId = UUID.randomUUID();
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
assertNull(invoiceWithMetadata.getInvoice());
}
@@ -1033,7 +1033,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
eventSet.add(createBillingEvent(subscriptionId2, bundleId, startDate, plan2, plan2phase1, 1));
// generate the first invoice
- final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, eventSet, invoices, startDate, currency, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, eventSet, invoices, null, startDate, currency, internalCallContext);
final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
assertNotNull(invoice1);
assertTrue(invoice1.getBalance().compareTo(FIFTEEN.add(TWELVE)) == 0);
@@ -1045,7 +1045,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
eventSet.addSubscriptionWithAutoInvoiceOff(subscriptionId1);
final LocalDate targetDate2 = startDate.plusMonths(1);
- final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, eventSet, invoices, targetDate2, currency, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, eventSet, invoices, null, targetDate2, currency, internalCallContext);
final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
assertNotNull(invoice2);
assertTrue(invoice2.getBalance().compareTo(TWELVE) == 0);
@@ -1054,7 +1054,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final LocalDate targetDate3 = targetDate2.plusMonths(1);
eventSet.clearSubscriptionsWithAutoInvoiceOff();
eventSet.add(subscription1creation);
- final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, eventSet, invoices, targetDate3, currency, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, eventSet, invoices, null, targetDate3, currency, internalCallContext);
final Invoice invoice3 = invoiceWithMetadata3.getInvoice();
assertNotNull(invoice3);
assertTrue(invoice3.getBalance().compareTo(FIFTEEN.multiply(TWO).add(TWELVE)) == 0);
@@ -1077,7 +1077,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3);
final UUID accountId = UUID.randomUUID();
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
assertNotNull(invoiceWithMetadata.getInvoice());
assertEquals(invoiceWithMetadata.getInvoice().getStatus(), InvoiceStatus.DRAFT);
@@ -1104,7 +1104,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
events.add(event);
final LocalDate targetDate = invoiceUtil.buildDate(2016, 10, 9);
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
@@ -1118,7 +1118,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
// Cancel EOT and Add the item adjustment
final BillingEvent event2 = invoiceUtil.createMockBillingEvent(account, sub, endDate.toDateTimeAtStartOfDay(),
- null, phase,
+ plan, phase,
ZERO, null, Currency.USD, BillingPeriod.NO_BILLING_PERIOD, 9,
BillingMode.IN_ADVANCE, "Cancel", 2L,
SubscriptionBaseTransitionType.CANCEL);
@@ -1130,7 +1130,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final List<Invoice> existingInvoices = new ArrayList<Invoice>();
existingInvoices.add(invoice);
- final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, existingInvoices, targetDate, Currency.USD, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, existingInvoices, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
assertNull(invoice2);
@@ -1392,7 +1392,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final LocalDate targetDate, final int expectedNumberOfItems,
final BigDecimal expectedAmount) throws InvoiceApiException {
final Currency currency = Currency.USD;
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, null, targetDate, currency, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNotNull(invoice);
assertEquals(invoice.getNumberOfItems(), expectedNumberOfItems);
@@ -1404,7 +1404,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
private void testNullInvoiceGeneration(final BillingEventSet events, final List<Invoice> existingInvoices, final LocalDate targetDate) throws InvoiceApiException {
final Currency currency = Currency.USD;
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, null, targetDate, currency, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
assertNull(invoice);
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/glue/TestInvoiceModule.java b/invoice/src/test/java/org/killbill/billing/invoice/glue/TestInvoiceModule.java
index bc4ddeb..5a8fdd3 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/glue/TestInvoiceModule.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/glue/TestInvoiceModule.java
@@ -25,14 +25,12 @@ import org.killbill.billing.mock.glue.MockTenantModule;
import org.killbill.billing.platform.api.KillbillConfigSource;
import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
import org.killbill.billing.usage.glue.UsageModule;
-import org.killbill.billing.util.email.EmailModule;
import org.killbill.billing.util.email.templates.TemplateModule;
import org.killbill.billing.util.glue.CacheModule;
import org.killbill.billing.util.glue.CallContextModule;
import org.killbill.billing.util.glue.ConfigModule;
import org.killbill.billing.util.glue.CustomFieldModule;
import org.killbill.billing.util.glue.MemoryGlobalLockerModule;
-import org.killbill.billing.util.glue.TagStoreModule;
import org.mockito.Mockito;
public class TestInvoiceModule extends DefaultInvoiceModule {
@@ -56,7 +54,6 @@ public class TestInvoiceModule extends DefaultInvoiceModule {
install(new CacheModule(configSource));
install(new ConfigModule(configSource));
install(new TemplateModule(configSource));
- install(new EmailModule(configSource));
install(new MockTenantModule(configSource));
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/MockBillingEventSet.java b/invoice/src/test/java/org/killbill/billing/invoice/MockBillingEventSet.java
index dabf129..2826c25 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/MockBillingEventSet.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/MockBillingEventSet.java
@@ -36,6 +36,7 @@ public class MockBillingEventSet extends TreeSet<BillingEvent> implements Billin
private boolean isAccountInvoiceOff;
private boolean isAccountAutoInvoiceDraft;
+ private boolean isAccountAutoInvoiceReuseDraft;
private List<UUID> subscriptionIdsWithAutoInvoiceOff;
@@ -43,6 +44,7 @@ public class MockBillingEventSet extends TreeSet<BillingEvent> implements Billin
super();
this.isAccountInvoiceOff = false;
this.isAccountAutoInvoiceDraft = false;
+ this.isAccountAutoInvoiceReuseDraft = false;
this.subscriptionIdsWithAutoInvoiceOff = new ArrayList<UUID>();
}
@@ -61,8 +63,8 @@ public class MockBillingEventSet extends TreeSet<BillingEvent> implements Billin
}
@Override
- public BillingMode getRecurringBillingMode() {
- return BillingMode.IN_ADVANCE;
+ public boolean isAccountAutoInvoiceReuseDraft() {
+ return isAccountAutoInvoiceReuseDraft;
}
@Override
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 5a86cc3..af1ceb2 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
@@ -37,18 +37,14 @@ import org.killbill.billing.catalog.api.Currency;
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;
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.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;
@@ -64,7 +60,6 @@ import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
@@ -99,9 +94,8 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
final LocalDate target = internalCallContext.toLocalDate(effectiveDate);
- final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
- internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
+ internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
null, invoiceConfig, clock, parkedAccountsManager);
Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), context);
@@ -143,9 +137,8 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
final LocalDate target = internalCallContext.toLocalDate(effectiveDate);
- final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
- internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
+ internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
null, invoiceConfig, clock, parkedAccountsManager);
// Verify initial tags state for account
@@ -291,9 +284,8 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
31, BillingMode.IN_ADVANCE, "CHANGE", 3L, SubscriptionBaseTransitionType.CHANGE));
Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<DryRunArguments>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
- final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
- internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
+ internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
null, invoiceConfig, clock, parkedAccountsManager);
final Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(account.getId(), new LocalDate("2012-07-30"), null, context);
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 e1df2df..6cf3f1a 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -57,7 +57,6 @@ 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;
-import org.killbill.billing.invoice.api.InvoiceNotifier;
import org.killbill.billing.invoice.api.InvoicePayment;
import org.killbill.billing.invoice.dao.InvoiceDao;
import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
@@ -68,7 +67,6 @@ import org.killbill.billing.invoice.dao.InvoicePaymentModelDao;
import org.killbill.billing.invoice.dao.InvoicePaymentSqlDao;
import org.killbill.billing.invoice.dao.InvoiceSqlDao;
import org.killbill.billing.invoice.generator.InvoiceGenerator;
-import org.killbill.billing.invoice.notification.NullInvoiceNotifier;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.junction.BillingEventSet;
import org.killbill.billing.junction.BillingInternalApi;
@@ -211,9 +209,8 @@ public class TestInvoiceHelper {
Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<DryRunArguments>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
- final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi,
- invoiceDao, internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
+ invoiceDao, internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
null, invoiceConfig, clock, parkedAccountsManager);
Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(account.getId(), targetDate, new DryRunFutureDateArguments(), internalCallContext);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
index 26f4170..49f3ce2 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -1493,6 +1493,32 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
Assert.assertEquals(previousExistingSize, 3);
}
+ @Test(groups = "fast")
+ public void testWithFreeRecurring() {
+ final LocalDate startDate = new LocalDate(2012, 8, 1);
+ final LocalDate endDate = new LocalDate(2012, 9, 1);
+
+ final BigDecimal monthlyRate1 = new BigDecimal("12.00");
+ final BigDecimal monthlyRate2 = new BigDecimal("24.00");
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+ final InvoiceItem freeMonthly = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, BigDecimal.ZERO, BigDecimal.ZERO, currency);
+ tree.addItem(freeMonthly);
+ final InvoiceItem payingMonthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyRate1, monthlyRate1, currency);
+ tree.addItem(payingMonthly1);
+ tree.flatten(true);
+
+ final InvoiceItem proposedPayingMonthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyRate2, monthlyRate2, currency);
+ tree.mergeProposedItem(proposedPayingMonthly2);
+ tree.buildForMerge();
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ expectedResult.add(proposedPayingMonthly2);
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, endDate, monthlyRate1.negate(), currency, payingMonthly1.getId());
+ expectedResult.add(repair);
+ verifyResult(tree.getView(), expectedResult);
+ }
+
private void printTreeJSON(final SubscriptionItemTree tree) throws IOException {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
tree.getRoot().jsonSerializeTree(OBJECT_MAPPER, outputStream);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceJson.java
index 90373da..36af2b2 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceJson.java
@@ -56,6 +56,8 @@ public class InvoiceJson extends JsonBase {
private final List<CreditJson> credits;
private final String status;
private final Boolean isParentInvoice;
+ private final String parentInvoiceId;
+ private final String parentAccountId;
@JsonCreator
public InvoiceJson(@JsonProperty("amount") final BigDecimal amount,
@@ -73,6 +75,8 @@ public class InvoiceJson extends JsonBase {
@JsonProperty("credits") final List<CreditJson> credits,
@JsonProperty("items") final List<InvoiceItemJson> items,
@JsonProperty("isParentInvoice") final Boolean isParentInvoice,
+ @JsonProperty("parentInvoiceId") final String parentInvoiceId,
+ @JsonProperty("parentAccountId") final String parentAccountId,
@JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
super(auditLogs);
this.amount = amount;
@@ -90,6 +94,8 @@ public class InvoiceJson extends JsonBase {
this.credits = credits;
this.items = items;
this.isParentInvoice = isParentInvoice;
+ this.parentInvoiceId = parentInvoiceId;
+ this.parentAccountId = parentAccountId;
}
public InvoiceJson(final Invoice input) {
@@ -99,7 +105,10 @@ public class InvoiceJson extends JsonBase {
public InvoiceJson(final Invoice input, final String bundleKeys, final List<CreditJson> credits, final List<AuditLog> auditLogs) {
this(input.getChargedAmount(), input.getCurrency().toString(), input.getStatus().toString(), input.getCreditedAmount(), input.getRefundedAmount(),
input.getId().toString(), input.getInvoiceDate(), input.getTargetDate(), String.valueOf(input.getInvoiceNumber()),
- input.getBalance(), input.getAccountId().toString(), bundleKeys, credits, null, input.isParentInvoice(), toAuditLogJson(auditLogs));
+ input.getBalance(), input.getAccountId().toString(), bundleKeys, credits, null, input.isParentInvoice(),
+ input.getParentInvoiceId() != null ? input.getParentInvoiceId().toString() : null,
+ input.getParentAccountId() != null ? input.getParentAccountId().toString() : null,
+ toAuditLogJson(auditLogs));
}
public InvoiceJson(final Invoice input, final boolean withItems, final List<InvoiceItem> childItems, @Nullable final AccountAuditLogs accountAuditLogs) {
@@ -133,6 +142,9 @@ public class InvoiceJson extends JsonBase {
this.bundleKeys = null;
this.credits = null;
this.isParentInvoice = input.isParentInvoice();
+ this.parentInvoiceId = input.getParentInvoiceId() != null ? input.getParentInvoiceId().toString() : null;
+ this.parentAccountId = input.getParentAccountId() != null ? input.getParentAccountId().toString() : null;
+
}
public BigDecimal getAmount() {
@@ -195,6 +207,14 @@ public class InvoiceJson extends JsonBase {
return isParentInvoice;
}
+ public String getParentInvoiceId() {
+ return parentInvoiceId;
+ }
+
+ public String getParentAccountId() {
+ return parentAccountId;
+ }
+
@Override
public String toString() {
return "InvoiceJson{" +
@@ -213,6 +233,8 @@ public class InvoiceJson extends JsonBase {
", bundleKeys='" + bundleKeys + '\'' +
", credits=" + credits +
", isParentInvoice=" + isParentInvoice +
+ ", parentInvoiceId=" + parentInvoiceId +
+ ", parentAccountId=" + parentAccountId +
'}';
}
@@ -272,7 +294,12 @@ public class InvoiceJson extends JsonBase {
if (isParentInvoice != null ? !isParentInvoice.equals(that.isParentInvoice) : that.isParentInvoice != null) {
return false;
}
-
+ if (parentInvoiceId != null ? !parentInvoiceId.equals(that.parentInvoiceId) : that.parentInvoiceId != null) {
+ return false;
+ }
+ if (parentAccountId != null ? !parentAccountId.equals(that.parentAccountId) : that.parentAccountId != null) {
+ return false;
+ }
return true;
}
@@ -293,6 +320,8 @@ public class InvoiceJson extends JsonBase {
result = 31 * result + (bundleKeys != null ? bundleKeys.hashCode() : 0);
result = 31 * result + (credits != null ? credits.hashCode() : 0);
result = 31 * result + (isParentInvoice != null ? isParentInvoice.hashCode() : 0);
+ result = 31 * result + (parentInvoiceId != null ? parentInvoiceId.hashCode() : 0);
+ result = 31 * result + (parentAccountId != null ? parentAccountId.hashCode() : 0);
return result;
}
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/ExceptionMapperBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/ExceptionMapperBase.java
index 95a7a37..c4b79cd 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/ExceptionMapperBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/ExceptionMapperBase.java
@@ -38,7 +38,6 @@ import org.killbill.billing.subscription.api.SubscriptionBillingApiException;
import org.killbill.billing.subscription.api.timeline.SubscriptionBaseRepairException;
import org.killbill.billing.util.api.TagApiException;
import org.killbill.billing.util.api.TagDefinitionApiException;
-import org.killbill.billing.util.email.EmailApiException;
import org.killbill.billing.util.jackson.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -75,9 +74,6 @@ public abstract class ExceptionMapperBase {
} else if (cause instanceof CatalogApiException) {
final CatalogApiExceptionMapper mapper = new CatalogApiExceptionMapper(uriInfo);
return mapper.toResponse((CatalogApiException) cause);
- } else if (cause instanceof EmailApiException) {
- final EmailApiExceptionMapper mapper = new EmailApiExceptionMapper(uriInfo);
- return mapper.toResponse((EmailApiException) cause);
} else if (cause instanceof EntitlementApiException) {
final EntitlementApiExceptionMapper mapper = new EntitlementApiExceptionMapper(uriInfo);
return mapper.toResponse((EntitlementApiException) cause);
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 efd0828..6cbcebb 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
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
@@ -121,6 +121,7 @@ import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.billing.util.config.definition.JaxrsConfig;
import org.killbill.billing.util.config.definition.PaymentConfig;
+import org.killbill.billing.util.customfield.CustomField;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.billing.util.tag.ControlTagType;
import org.killbill.billing.util.tag.Tag;
@@ -820,6 +821,7 @@ public class AccountResource extends JaxRsResourceBase {
@PathParam("accountId") final String accountIdStr,
@QueryParam(QUERY_PAYMENT_METHOD_IS_DEFAULT) @DefaultValue("false") final Boolean isDefault,
@QueryParam(QUERY_PAY_ALL_UNPAID_INVOICES) @DefaultValue("false") final Boolean payAllUnpaidInvoices,
+ @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List<String> paymentControlPluginNames,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
@HeaderParam(HDR_CREATED_BY) final String createdBy,
@HeaderParam(HDR_REASON) final String reason,
@@ -844,7 +846,9 @@ public class AccountResource extends JaxRsResourceBase {
return Response.status(Status.BAD_REQUEST).build();
}
- final UUID paymentMethodId = paymentApi.addPaymentMethod(account, data.getExternalKey(), data.getPluginName(), isDefault, data.getPluginDetail(), pluginProperties, callContext);
+
+ final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
+ final UUID paymentMethodId = paymentApi.addPaymentMethodWithPaymentControl(account, data.getExternalKey(), data.getPluginName(), isDefault, data.getPluginDetail(), pluginProperties, paymentOptions, callContext);
if (payAllUnpaidInvoices && unpaidInvoices.size() > 0) {
for (final Invoice invoice : unpaidInvoices) {
createPurchaseForInvoice(account, invoice.getId(), invoice.getBalance(), paymentMethodId, false, null, null, pluginProperties, callContext);
@@ -1190,6 +1194,26 @@ public class AccountResource extends JaxRsResourceBase {
}
@TimedResource
+ @GET
+ @Path("/{accountId:" + UUID_PATTERN + "}/" + ALL_CUSTOM_FIELDS)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Retrieve account customFields", response = CustomFieldJson.class, responseContainer = "List")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
+ @ApiResponse(code = 404, message = "Account not found")})
+ public Response getAllCustomFields(@PathParam(ID_PARAM_NAME) final String accountIdString,
+ @QueryParam(QUERY_OBJECT_TYPE) final ObjectType objectType,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) {
+ final UUID accountId = UUID.fromString(accountIdString);
+
+ final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
+ final List<CustomField> customFields = objectType != null ?
+ customFieldUserApi.getCustomFieldsForAccountType(accountId, objectType, tenantContext) :
+ customFieldUserApi.getCustomFieldsForAccount(accountId, tenantContext);
+ return createCustomFieldResponse(customFields, auditMode, tenantContext);
+ }
+
+ @TimedResource
@POST
@Path("/{accountId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
@Consumes(APPLICATION_JSON)
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
index fea4aa9..4eea0e9 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
@@ -221,7 +221,7 @@ public class CatalogResource extends JaxRsResourceBase {
simplePlan.getTrialLength(),
simplePlan.getTrialTimeUnit(),
simplePlan.getAvailableBaseProducts());
- catalogUserApi.addSimplePlan(desc, clock.getUTCNow(), callContext);
+ catalogUserApi.addSimplePlan(desc, null, callContext);
return uriBuilder.buildResponse(uriInfo, CatalogResource.class, null, null, request);
}
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 2e815f3..4592153 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
@@ -73,7 +73,6 @@ 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;
-import org.killbill.billing.invoice.api.InvoiceNotifier;
import org.killbill.billing.invoice.api.InvoicePayment;
import org.killbill.billing.invoice.api.InvoiceUserApi;
import org.killbill.billing.jaxrs.json.CustomFieldJson;
@@ -136,7 +135,6 @@ public class InvoiceResource extends JaxRsResourceBase {
private static final String LOCALE_PARAM_NAME = "locale";
private final InvoiceUserApi invoiceApi;
- private final InvoiceNotifier invoiceNotifier;
private final TenantUserApi tenantApi;
private final Locale defaultLocale;
@@ -151,7 +149,6 @@ public class InvoiceResource extends JaxRsResourceBase {
public InvoiceResource(final AccountUserApi accountUserApi,
final InvoiceUserApi invoiceApi,
final PaymentApi paymentApi,
- final InvoiceNotifier invoiceNotifier,
final Clock clock,
final JaxrsUriBuilder uriBuilder,
final TagUserApi tagUserApi,
@@ -161,7 +158,6 @@ public class InvoiceResource extends JaxRsResourceBase {
final Context context) {
super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, paymentApi, null, clock, context);
this.invoiceApi = invoiceApi;
- this.invoiceNotifier = invoiceNotifier;
this.tenantApi = tenantApi;
this.defaultLocale = Locale.getDefault();
}
@@ -341,12 +337,11 @@ public class InvoiceResource extends JaxRsResourceBase {
final Account account = accountUserApi.getAccountById(accountId, callContext);
final Iterable<InvoiceItem> sanitizedInvoiceItems = validateSanitizeAndTranformInputItems(account.getCurrency(), items);
- final LocalDate resolvedTargetDate = toLocalDateDefaultToday(account, targetDate, callContext);
+ final LocalDate resolvedTargetDate = toLocalDateDefaultToday(account, targetDate, callContext);
final UUID invoiceId = invoiceApi.createMigrationInvoice(accountId, resolvedTargetDate, sanitizedInvoiceItems, callContext);
return uriBuilder.buildResponse(uriInfo, InvoiceResource.class, "getInvoice", invoiceId, request);
}
-
@TimedResource
@POST
@Path("/" + DRY_RUN)
@@ -663,7 +658,6 @@ public class InvoiceResource extends JaxRsResourceBase {
payment.getPurchasedAmount(), "InvoicePaymentJson purchasedAmount needs to be set");
Preconditions.checkArgument(!externalPayment || payment.getPaymentMethodId() == null, "InvoicePaymentJson should not contain a paymentMethodId when this is an external payment");
-
final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
final CallContext callContext = context.createCallContextNoAccountId(createdBy, reason, comment, request);
@@ -681,34 +675,6 @@ public class InvoiceResource extends JaxRsResourceBase {
}
@TimedResource
- @POST
- @Path("/{invoiceId:" + UUID_PATTERN + "}/" + EMAIL_NOTIFICATIONS)
- @Consumes(APPLICATION_JSON)
- @Produces(APPLICATION_JSON)
- @ApiOperation(value = "Trigger an email notification for invoice")
- @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid invoice id supplied"),
- @ApiResponse(code = 404, message = "Account or invoice not found")})
- public Response triggerEmailNotificationForInvoice(@PathParam("invoiceId") final String invoiceId,
- @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 HttpServletRequest request) throws InvoiceApiException, AccountApiException {
- final CallContext callContext = context.createCallContextNoAccountId(createdBy, reason, comment, request);
-
- final Invoice invoice = invoiceApi.getInvoice(UUID.fromString(invoiceId), callContext);
- if (invoice == null) {
- throw new InvoiceApiException(ErrorCode.INVOICE_NOT_FOUND, invoiceId);
- }
-
- final Account account = accountUserApi.getAccountById(invoice.getAccountId(), callContext);
-
- // Send the email (synchronous send)
- invoiceNotifier.notify(account, invoice, callContext);
-
- return Response.status(Status.OK).build();
- }
-
- @TimedResource
@GET
@Path("/" + INVOICE_TRANSLATION + "/{locale:" + ANYTHING_PATTERN + "}/")
@Produces(TEXT_PLAIN)
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 38a3986..5bb10b5 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
@@ -221,6 +221,7 @@ public interface JaxrsResource {
public static final String TAGS = "tags";
public static final String TAGS_PATH = PREFIX + "/" + TAGS;
+ public static final String ALL_CUSTOM_FIELDS = "allCustomFields";
public static final String CUSTOM_FIELDS = "customFields";
public static final String CUSTOM_FIELDS_PATH = PREFIX + "/" + CUSTOM_FIELDS;
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 79a3744..97c9b8c 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
@@ -241,8 +241,11 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
protected Response getCustomFields(final UUID id, final AuditMode auditMode, final TenantContext context) {
final List<CustomField> fields = customFieldUserApi.getCustomFieldsForObject(id, getObjectType(), context);
+ return createCustomFieldResponse(fields, auditMode, context);
+ }
- final List<CustomFieldJson> result = new LinkedList<CustomFieldJson>();
+ protected Response createCustomFieldResponse(final Iterable<CustomField> fields, final AuditMode auditMode, final TenantContext context) {
+ final Collection<CustomFieldJson> result = new LinkedList<CustomFieldJson>();
for (final CustomField cur : fields) {
// TODO PIERRE - Bulk API
final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(cur.getId(), ObjectType.CUSTOM_FIELD, auditMode.getLevel(), context);
@@ -350,12 +353,11 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
}
protected void validatePaymentMethodForAccount(final UUID accountId, final UUID paymentMethodId, final CallContext callContext) throws PaymentApiException {
- if (paymentMethodId == null) {
- throw new PaymentApiException(ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD, accountId);
- }
- final PaymentMethod paymentMethod = paymentApi.getPaymentMethodById(paymentMethodId, false, false, ImmutableList.<PluginProperty>of(), callContext);
- if (!paymentMethod.getAccountId().equals(accountId)) {
- throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD, paymentMethodId);
+ if (paymentMethodId != null) {
+ final PaymentMethod paymentMethod = paymentApi.getPaymentMethodById(paymentMethodId, false, false, ImmutableList.<PluginProperty>of(), callContext);
+ if (!paymentMethod.getAccountId().equals(accountId)) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD, paymentMethodId);
+ }
}
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
index 0d0f00c..f4b0af2 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
@@ -555,10 +555,11 @@ public class SubscriptionResource extends JaxRsResourceBase {
final Entitlement newEntitlement;
final Account account = accountUserApi.getAccountById(current.getAccountId(), callContext);
- final PlanSpecifier planSpec = entitlement.getPlanName() != null ?
- new PlanSpecifier(entitlement.getPlanName()) :
- new PlanSpecifier(entitlement.getProductName(),
- BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList());
+ final PhaseType phaseType = entitlement.getPhaseType() != null ? PhaseType.valueOf(entitlement.getPhaseType()) : null;
+ final PlanPhaseSpecifier planSpec = entitlement.getPlanName() != null ?
+ new PlanPhaseSpecifier(entitlement.getPlanName(), phaseType) :
+ new PlanPhaseSpecifier(entitlement.getProductName(),
+ BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), phaseType);
final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(entitlement.getPriceOverrides(), planSpec, account.getCurrency());
if (requestedDate == null && policyString == null) {
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
index b0cc317..664fcea 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
@@ -37,6 +37,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
+import org.joda.time.DateTime;
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.AccountUserApi;
import org.killbill.billing.callcontext.DefaultCallContext;
@@ -140,11 +141,13 @@ public class TenantResource extends JaxRsResourceBase {
if (!useGlobalDefault) {
final CallContext callContext = new DefaultCallContext(null, tenant.getId(), createdBy, CallOrigin.EXTERNAL,
UserType.CUSTOMER, Context.getOrCreateUserToken(), clock);
- catalogUserApi.createDefaultEmptyCatalog(clock.getUTCNow(),callContext);
+
+ catalogUserApi.createDefaultEmptyCatalog(null, callContext);
}
return uriBuilder.buildResponse(uriInfo, TenantResource.class, "getTenant", tenant.getId(), request);
}
+
@TimedResource
@POST
@Path("/" + REGISTER_NOTIFICATION_CALLBACK)
@@ -420,4 +423,5 @@ public class TenantResource extends JaxRsResourceBase {
protected ObjectType getObjectType() {
return ObjectType.TENANT;
}
+
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java
index 35c70d4..a73a9b9 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java
@@ -268,7 +268,7 @@ public class TestResource extends JaxRsResourceBase {
private boolean areAllBusEventsProcessed(final Long tenantRecordId) {
final Iterable<BusEventWithMetadata<BusEvent>> availableBusEventForSearchKey2 = persistentBus.getAvailableOrInProcessingBusEventsForSearchKey2(null, tenantRecordId);
- final int nbBusEvents = Iterables.<BusEventWithMetadata<BusEvent>>size(availableBusEventForSearchKey2);
+ final int nbBusEvents = Iterables.size(availableBusEventForSearchKey2);
if (nbBusEvents != 0) {
log.info("TestResource: at least {} more bus event(s) to process", nbBusEvents);
}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
index 6b9a29e..5dc7791 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
@@ -54,7 +54,7 @@ public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuiteNoDB {
final InvoiceJson invoiceJsonSimple = new InvoiceJson(amount, Currency.USD.toString(), InvoiceStatus.COMMITTED.toString(),
creditAdj, refundAdj, invoiceId, invoiceDate,
targetDate, invoiceNumber, balance, accountId, bundleKeys,
- credits, null, false, auditLogs);
+ credits, null, false, null, null, auditLogs);
Assert.assertEquals(invoiceJsonSimple.getAmount(), amount);
Assert.assertEquals(invoiceJsonSimple.getCreditAdj(), creditAdj);
Assert.assertEquals(invoiceJsonSimple.getRefundAdj(), refundAdj);
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 35bda36..0522fb1 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
@@ -42,13 +42,14 @@ public class DefaultBillingEventSet extends TreeSet<BillingEvent> implements Sor
private final boolean accountAutoInvoiceOff;
private final boolean accountAutoInvoiceDraft;
+ private final boolean accountAutoInvoiceReuseDraft;
private final List<UUID> subscriptionIdsWithAutoInvoiceOff;
- private final BillingMode recurringBillingMode;
- public DefaultBillingEventSet(final boolean accountAutoInvoiceOff, final boolean accountAutoInvoiceDraft, final BillingMode recurringBillingMode) {
+
+ public DefaultBillingEventSet(final boolean accountAutoInvoiceOff, final boolean accountAutoInvoiceDraft, final boolean accountAutoInvoiceReuseDraft) {
this.accountAutoInvoiceOff = accountAutoInvoiceOff;
this.accountAutoInvoiceDraft = accountAutoInvoiceDraft;
- this.recurringBillingMode = recurringBillingMode;
+ this.accountAutoInvoiceReuseDraft = accountAutoInvoiceReuseDraft;
this.subscriptionIdsWithAutoInvoiceOff = new ArrayList<UUID>();
}
@@ -63,8 +64,8 @@ public class DefaultBillingEventSet extends TreeSet<BillingEvent> implements Sor
}
@Override
- public BillingMode getRecurringBillingMode() {
- return recurringBillingMode;
+ public boolean isAccountAutoInvoiceReuseDraft() {
+ return accountAutoInvoiceReuseDraft;
}
@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 b8f0bac..a3499b5 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
@@ -96,17 +96,18 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
final List<Tag> accountTags = tagApi.getTags(accountId, ObjectType.ACCOUNT, context);
final boolean found_AUTO_INVOICING_OFF = is_AUTO_INVOICING_OFF(accountTags);
final boolean found_INVOICING_DRAFT = is_AUTO_INVOICING_DRAFT(accountTags);
+ final boolean found_INVOICING_REUSE_DRAFT = is_AUTO_INVOICING_REUSE_DRAFT(accountTags);
final Set<UUID> skippedSubscriptions = new HashSet<UUID>();
final DefaultBillingEventSet result;
if (found_AUTO_INVOICING_OFF) {
- result = new DefaultBillingEventSet(true, found_INVOICING_DRAFT, ((StaticCatalog) currentCatalog).getRecurringBillingMode()); // billing is off, we are done
+ result = new DefaultBillingEventSet(true, found_INVOICING_DRAFT, found_INVOICING_REUSE_DRAFT); // billing is off, we are done
} else {
final List<SubscriptionBaseBundle> bundles = subscriptionApi.getBundlesForAccount(accountId, context);
final ImmutableAccountData account = accountApi.getImmutableAccountDataById(accountId, context);
- result = new DefaultBillingEventSet(false, found_INVOICING_DRAFT, ((StaticCatalog) currentCatalog).getRecurringBillingMode());
+ result = new DefaultBillingEventSet(false, found_INVOICING_DRAFT, found_INVOICING_REUSE_DRAFT);
addBillingEventsForBundles(bundles, account, dryRunArguments, context, result, skippedSubscriptions, currentCatalog);
}
@@ -259,4 +260,15 @@ public class DefaultInternalBillingApi implements BillingInternalApi {
}
});
}
+
+ private boolean is_AUTO_INVOICING_REUSE_DRAFT(final List<Tag> tags) {
+ return Iterables.any(tags, new Predicate<Tag>() {
+ @Override
+ public boolean apply(final Tag input) {
+ return input.getTagDefinitionId().equals(ControlTagType.AUTO_INVOICING_REUSE_DRAFT.getId());
+ }
+ });
+ }
+
+
}
NEWS 6(+6 -0)
diff --git a/NEWS b/NEWS
index 94d650c..c1e7da5 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,9 @@
+0.18.13
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.18.13
+
+0.18.12
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.18.12
+
0.18.11
See https://github.com/killbill/killbill/releases/tag/killbill-0.18.11
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 bdab42c..b880b1d 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
@@ -18,7 +18,6 @@
package org.killbill.billing.overdue.applicator;
-import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
@@ -30,12 +29,10 @@ import org.joda.time.DateTime;
import org.joda.time.Period;
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;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.EntitlementInternalApi;
@@ -63,23 +60,16 @@ import org.killbill.billing.tag.TagInternalApi;
import org.killbill.billing.util.api.TagApiException;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
-import org.killbill.billing.util.email.DefaultEmailSender;
-import org.killbill.billing.util.email.EmailApiException;
-import org.killbill.billing.util.email.EmailConfig;
-import org.killbill.billing.util.email.EmailSender;
import org.killbill.billing.util.tag.ControlTagType;
import org.killbill.billing.util.tag.Tag;
import org.killbill.bus.api.PersistentBus;
-import org.killbill.clock.Clock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Predicate;
-import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
-import com.samskivert.mustache.MustacheException;
public class OverdueStateApplicator {
@@ -91,9 +81,7 @@ public class OverdueStateApplicator {
private final AccountInternalApi accountApi;
private final EntitlementApi entitlementApi;
private final EntitlementInternalApi entitlementInternalApi;
- private final OverdueEmailGenerator overdueEmailGenerator;
private final TagInternalApi tagApi;
- private final EmailSender emailSender;
private final InternalCallContextFactory internalCallContextFactory;
@Inject
@@ -102,8 +90,6 @@ public class OverdueStateApplicator {
final EntitlementApi entitlementApi,
final EntitlementInternalApi entitlementInternalApi,
@Named(DefaultOverdueModule.OVERDUE_NOTIFIER_CHECK_NAMED) final OverduePoster checkPoster,
- final OverdueEmailGenerator overdueEmailGenerator,
- final EmailConfig config,
final PersistentBus bus,
final TagInternalApi tagApi,
final InternalCallContextFactory internalCallContextFactory) {
@@ -113,60 +99,52 @@ public class OverdueStateApplicator {
this.entitlementApi = entitlementApi;
this.entitlementInternalApi = entitlementInternalApi;
this.checkPoster = checkPoster;
- this.overdueEmailGenerator = overdueEmailGenerator;
this.tagApi = tagApi;
this.internalCallContextFactory = internalCallContextFactory;
- this.emailSender = new DefaultEmailSender(config);
this.bus = bus;
}
public void apply(final DateTime effectiveDate, final OverdueStateSet overdueStateSet, final BillingState billingState,
final ImmutableAccountData account, final OverdueState previousOverdueState,
final OverdueState nextOverdueState, final InternalCallContext context) throws OverdueException, OverdueApiException {
- try {
- if (isAccountTaggedWith_OVERDUE_ENFORCEMENT_OFF(context)) {
- log.debug("OverdueStateApplicator: apply returns because account (recordId={}) is set with OVERDUE_ENFORCEMENT_OFF", context.getAccountRecordId());
- return;
- }
+ if (isAccountTaggedWith_OVERDUE_ENFORCEMENT_OFF(context)) {
+ log.debug("OverdueStateApplicator: apply returns because account (recordId={}) is set with OVERDUE_ENFORCEMENT_OFF", context.getAccountRecordId());
+ return;
+ }
- log.debug("OverdueStateApplicator: time={}, previousState={}, nextState={}, billingState={}", effectiveDate, previousOverdueState, nextOverdueState, billingState);
-
- final OverdueState firstOverdueState = overdueStateSet.getFirstState();
- final boolean conditionForNextNotfication = !nextOverdueState.isClearState() ||
- // We did not reach the first state yet but we have an unpaid invoice
- (firstOverdueState != null && billingState != null && billingState.getDateOfEarliestUnpaidInvoice() != null);
-
- if (conditionForNextNotfication) {
- final Period reevaluationInterval = getReevaluationInterval(overdueStateSet, nextOverdueState);
- // If there is no configuration in the config, we assume this is because the overdue conditions are not time based and so there is nothing to retry
- if (reevaluationInterval == null) {
- log.debug("OverdueStateApplicator <notificationQ>: missing InitialReevaluationInterval from config, NOT inserting notification for account {}", account.getId());
- } else {
- log.debug("OverdueStateApplicator <notificationQ>: inserting notification for account={}, time={}", account.getId(), effectiveDate.plus(reevaluationInterval));
- createFutureNotification(account, effectiveDate.plus(reevaluationInterval), context);
- }
- } else if (nextOverdueState.isClearState()) {
- clearFutureNotification(account, context);
- }
+ log.debug("OverdueStateApplicator: time={}, previousState={}, nextState={}, billingState={}", effectiveDate, previousOverdueState, nextOverdueState, billingState);
+
+ final OverdueState firstOverdueState = overdueStateSet.getFirstState();
+ final boolean conditionForNextNotfication = !nextOverdueState.isClearState() ||
+ // We did not reach the first state yet but we have an unpaid invoice
+ (firstOverdueState != null && billingState != null && billingState.getDateOfEarliestUnpaidInvoice() != null);
- if (previousOverdueState.getName().equals(nextOverdueState.getName())) {
- log.debug("OverdueStateApplicator is no-op: previousState={}, nextState={}", previousOverdueState, nextOverdueState);
- return;
+ if (conditionForNextNotfication) {
+ final Period reevaluationInterval = getReevaluationInterval(overdueStateSet, nextOverdueState);
+ // If there is no configuration in the config, we assume this is because the overdue conditions are not time based and so there is nothing to retry
+ if (reevaluationInterval == null) {
+ log.debug("OverdueStateApplicator <notificationQ>: missing InitialReevaluationInterval from config, NOT inserting notification for account {}", account.getId());
+ } else {
+ log.debug("OverdueStateApplicator <notificationQ>: inserting notification for account={}, time={}", account.getId(), effectiveDate.plus(reevaluationInterval));
+ createFutureNotification(account, effectiveDate.plus(reevaluationInterval), context);
}
+ } else if (nextOverdueState.isClearState()) {
+ clearFutureNotification(account, context);
+ }
- cancelSubscriptionsIfRequired(effectiveDate, account, nextOverdueState, context);
+ if (previousOverdueState.getName().equals(nextOverdueState.getName())) {
+ log.debug("OverdueStateApplicator is no-op: previousState={}, nextState={}", previousOverdueState, nextOverdueState);
+ return;
+ }
- sendEmailIfRequired(account.getId(), billingState, nextOverdueState, context);
+ cancelSubscriptionsIfRequired(effectiveDate, account, nextOverdueState, context);
- avoid_extra_credit_by_toggling_AUTO_INVOICE_OFF(account, previousOverdueState, nextOverdueState, context);
+ avoid_extra_credit_by_toggling_AUTO_INVOICE_OFF(account, previousOverdueState, nextOverdueState, context);
- // Make sure to store the new state last here: the entitlement DAO will send a BlockingTransitionInternalEvent
- // on the bus to which invoice will react. We need the latest state (including AUTO_INVOICE_OFF tag for example)
- // to be present in the database first.
- storeNewState(effectiveDate, account, nextOverdueState, context);
- } catch (final AccountApiException e) {
- throw new OverdueException(e);
- }
+ // Make sure to store the new state last here: the entitlement DAO will send a BlockingTransitionInternalEvent
+ // on the bus to which invoice will react. We need the latest state (including AUTO_INVOICE_OFF tag for example)
+ // to be present in the database first.
+ storeNewState(effectiveDate, account, nextOverdueState, context);
final OverdueChangeInternalEvent event;
try {
@@ -354,45 +332,6 @@ public class OverdueStateApplicator {
result.addAll(allEntitlementsButAddonsForAccountId);
}
- 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);
-
- // If sending is not configured, skip
- if (nextOverdueState.getEmailNotification() == null) {
- 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;
- }
-
- final List<String> to = ImmutableList.<String>of(account.getEmail());
- // TODO - should we look at the account CC: list?
- final List<String> cc = ImmutableList.<String>of();
- final String subject = nextOverdueState.getEmailNotification().getSubject();
-
- try {
- // Generate and send the email
- final String emailBody = overdueEmailGenerator.generateEmail(account, billingState, account, nextOverdueState);
- if (nextOverdueState.getEmailNotification().isHTML()) {
- emailSender.sendHTMLEmail(to, cc, subject, emailBody);
- } else {
- emailSender.sendPlainTextEmail(to, cc, subject, emailBody);
- }
- } catch (final IOException e) {
- log.warn("Unable to generate or send overdue notification email for accountId='{}'", account.getId(), e);
- } catch (final EmailApiException e) {
- log.warn("Unable to send overdue notification email for accountId='{}'", account.getId(), e);
- } catch (final MustacheException e) {
- log.warn("Unable to generate overdue notification email for accountId='{}'", account.getId(), e);
- }
- }
-
//
// Uses callcontext information to retrieve account matching the Overduable object and check whether we should do any overdue processing
//
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultEmailNotification.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultEmailNotification.java
index ea314cb..c7167b7 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultEmailNotification.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultEmailNotification.java
@@ -22,8 +22,9 @@ import javax.xml.bind.annotation.XmlElement;
import org.killbill.billing.overdue.api.EmailNotification;
+@Deprecated // Not used, just kept for config compatibility
@XmlAccessorType(XmlAccessType.NONE)
-public class DefaultEmailNotification implements EmailNotification {
+public class DefaultEmailNotification {
@XmlElement(required = true, name = "subject")
private String subject;
@@ -33,19 +34,4 @@ public class DefaultEmailNotification implements EmailNotification {
@XmlElement(required = false, name = "isHTML")
private Boolean isHTML = false;
-
- @Override
- public String getSubject() {
- return subject;
- }
-
- @Override
- public String getTemplateName() {
- return templateName;
- }
-
- @Override
- public Boolean isHTML() {
- return isHTML;
- }
}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java
index 6c57ffd..233a0ca 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java
@@ -18,8 +18,11 @@
package org.killbill.billing.overdue.config;
+import java.util.List;
+
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlID;
@@ -68,6 +71,7 @@ public class DefaultOverdueState extends ValidatingConfig<DefaultOverdueConfig>
@XmlElement(required = false, name = "autoReevaluationInterval")
private DefaultDuration autoReevaluationInterval;
+ @Deprecated // Not used, just kept for config compatibility
@XmlElement(required = false, name = "enterStateEmailNotification")
private DefaultEmailNotification enterStateEmailNotification;
@@ -174,10 +178,6 @@ public class DefaultOverdueState extends ValidatingConfig<DefaultOverdueConfig>
return errors;
}
- @Override
- public EmailNotification getEmailNotification() {
- return enterStateEmailNotification;
- }
@Override
public String toString() {
@@ -190,7 +190,6 @@ public class DefaultOverdueState extends ValidatingConfig<DefaultOverdueConfig>
sb.append(", subscriptionCancellationPolicy=").append(subscriptionCancellationPolicy);
sb.append(", isClearState=").append(isClearState);
sb.append(", autoReevaluationInterval=").append(autoReevaluationInterval);
- sb.append(", enterStateEmailNotification=").append(enterStateEmailNotification);
sb.append('}');
return sb.toString();
}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStatesAccount.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStatesAccount.java
index 004fd2d..c384444 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStatesAccount.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStatesAccount.java
@@ -16,6 +16,9 @@
package org.killbill.billing.overdue.config;
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlElement;
import org.joda.time.Period;
@@ -32,6 +35,7 @@ public class DefaultOverdueStatesAccount extends DefaultOverdueStateSet implemen
@XmlElement(required = true, name = "state")
private DefaultOverdueState[] accountOverdueStates = new DefaultOverdueState[0];
+
@Override
public DefaultOverdueState[] getStates() {
return accountOverdueStates;
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/glue/DefaultOverdueModule.java b/overdue/src/main/java/org/killbill/billing/overdue/glue/DefaultOverdueModule.java
index 930c774..9a00618 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/glue/DefaultOverdueModule.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/glue/DefaultOverdueModule.java
@@ -23,9 +23,6 @@ import org.killbill.billing.overdue.OverdueProperties;
import org.killbill.billing.overdue.OverdueService;
import org.killbill.billing.overdue.api.DefaultOverdueApi;
import org.killbill.billing.overdue.api.OverdueApi;
-import org.killbill.billing.overdue.applicator.OverdueEmailGenerator;
-import org.killbill.billing.overdue.applicator.formatters.DefaultOverdueEmailFormatterFactory;
-import org.killbill.billing.overdue.applicator.formatters.OverdueEmailFormatterFactory;
import org.killbill.billing.overdue.caching.EhCacheOverdueConfigCache;
import org.killbill.billing.overdue.caching.OverdueCacheInvalidationCallback;
import org.killbill.billing.overdue.caching.OverdueConfigCache;
@@ -64,7 +61,6 @@ public class DefaultOverdueModule extends KillBillModule implements OverdueModul
// internal bindings
installOverdueService();
installOverdueWrapperFactory();
- installOverdueEmail();
final OverdueProperties config = new ConfigurationObjectFactory(skifeConfigSource).build(OverdueProperties.class);
bind(OverdueProperties.class).toInstance(config);
@@ -86,11 +82,6 @@ public class DefaultOverdueModule extends KillBillModule implements OverdueModul
bind(OverdueWrapperFactory.class).asEagerSingleton();
}
- protected void installOverdueEmail() {
- bind(OverdueEmailFormatterFactory.class).to(DefaultOverdueEmailFormatterFactory.class).asEagerSingleton();
- bind(OverdueEmailGenerator.class).asEagerSingleton();
- }
-
@Override
public void installOverdueUserApi() {
bind(OverdueApi.class).to(DefaultOverdueApi.class).asEagerSingleton();
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusPoster.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusPoster.java
index 124fe69..9876898 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusPoster.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusPoster.java
@@ -50,6 +50,6 @@ public class OverdueAsyncBusPoster extends DefaultOverduePosterBase {
// Note that this is slightly incorrect because we could for instance already have a REFRESH and insert a CLEAR, but if that were the case,
// if means overdue state would change very rapidly and the behavior would anyway be non deterministic
// Note: don't use isEmpty() to go through all results to close the connection
- return Iterables.<NotificationEventWithMetadata<T>>size(futureNotifications) == 0;
+ return Iterables.size(futureNotifications) == 0;
}
}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/config/TestOverdueConfig.java b/overdue/src/test/java/org/killbill/billing/overdue/config/TestOverdueConfig.java
index d4326db..f0209b9 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/config/TestOverdueConfig.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/config/TestOverdueConfig.java
@@ -70,16 +70,9 @@ public class TestOverdueConfig extends OverdueTestSuiteNoDB {
final DefaultOverdueConfig c = XMLLoader.getObjectFromStreamNoValidation(is, DefaultOverdueConfig.class);
Assert.assertEquals(c.getOverdueStatesAccount().size(), 2);
- Assert.assertNull(c.getOverdueStatesAccount().getStates()[1].getEmailNotification());
-
Assert.assertNotNull(c.getOverdueStatesAccount().getInitialReevaluationInterval());
Assert.assertEquals(c.getOverdueStatesAccount().getInitialReevaluationInterval().getDays(), 1);
- final EmailNotification secondNotification = c.getOverdueStatesAccount().getStates()[0].getEmailNotification();
- Assert.assertEquals(secondNotification.getSubject(), "ToTo");
- Assert.assertEquals(secondNotification.getTemplateName(), "Titi");
- Assert.assertFalse(secondNotification.isHTML());
-
Assert.assertEquals(c.getOverdueStatesAccount().getFirstState().getName(), "OD1");
}
}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java b/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java
index 8d603af..21943cd 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java
@@ -40,7 +40,6 @@ import org.killbill.billing.overdue.caching.OverdueConfigCache;
import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.killbill.billing.platform.api.KillbillConfigSource;
import org.killbill.billing.tenant.api.TenantInternalApi.CacheInvalidationCallback;
-import org.killbill.billing.util.email.EmailModule;
import org.killbill.billing.util.email.templates.TemplateModule;
import org.killbill.billing.util.glue.AuditModule;
import org.killbill.billing.util.glue.CacheModule;
@@ -66,7 +65,6 @@ public class TestOverdueModule extends DefaultOverdueModule {
install(new ConfigModule(configSource));
install(new CallContextModule(configSource));
install(new CustomFieldModule(configSource));
- install(new EmailModule(configSource));
install(new MockAccountModule(configSource));
install(new MockEntitlementModule(configSource, new ApplicatorBlockingApi()));
install(new MockInvoiceModule(configSource));
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java
index 3f8cd00..1a7b3bd 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java
@@ -72,7 +72,7 @@ public class TestDefaultOverdueCheckPoster extends OverdueTestSuiteWithEmbeddedD
insertOverdueCheckAndVerifyQueueContent(overdueable, 15, 5);
// Verify the final content of the queue
- Assert.assertEquals(Iterables.<NotificationEventWithMetadata>size(overdueQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId())), 1);
+ Assert.assertEquals(Iterables.size(overdueQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId())), 1);
}
private void insertOverdueCheckAndVerifyQueueContent(final Account account, final int nbDaysInFuture, final int expectedNbDaysInFuture) throws IOException {
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueCheckNotifier.java b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueCheckNotifier.java
index 162d756..744e845 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueCheckNotifier.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueCheckNotifier.java
@@ -91,7 +91,7 @@ public class TestOverdueCheckNotifier extends OverdueTestSuiteWithEmbeddedDB {
final UUID accountId = new UUID(0L, 1L);
final Account account = Mockito.mock(Account.class);
Mockito.when(account.getId()).thenReturn(accountId);
- Mockito.when(accountApi.getImmutableAccountDataByRecordId(Mockito.<UUID>eq(internalCallContext.getAccountRecordId()), Mockito.<InternalTenantContext>any())).thenReturn(account);
+ Mockito.when(accountApi.getImmutableAccountDataByRecordId(Mockito.eq(internalCallContext.getAccountRecordId()), Mockito.<InternalTenantContext>any())).thenReturn(account);
final DateTime now = clock.getUTCNow();
final DateTime readyTime = now.plusMillis(2000);
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 34a3561..8c55462 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
@@ -116,7 +116,7 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
}
@Override
- public Payment createAuthorizationWithPaymentControl(final Account account, final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency,
+ public Payment createAuthorizationWithPaymentControl(final Account account, @Nullable final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency,
@Nullable final String paymentExternalKey, @Nullable final String paymentTransactionExternalKey,
final Iterable<PluginProperty> properties, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
final List<String> paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions, callContext);
@@ -325,17 +325,12 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
checkNotNullParameter(properties, "plugin properties");
checkExternalKeyLength(paymentTransactionExternalKey);
- if (paymentMethodId == null && !paymentOptions.isExternalPayment()) {
- throw new PaymentApiException(ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD, "paymentMethodId", "should not be null");
- }
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
- // TODO validate if the code is located properly here
- // The code should understand that the external payment method needs to be created
- // if it doesn't exist yet and trigger a CREDIT using the (built-in) external payment plugin.
- final UUID nonNullPaymentMethodId = (paymentMethodId != null) ?
- paymentMethodId :
- paymentMethodProcessor.createOrGetExternalPaymentMethod(UUIDs.randomUUID().toString(), account, properties, callContext, internalCallContext);
+
+ final UUID resolvedPaymentMethodId = (paymentMethodId == null && paymentOptions.isExternalPayment()) ?
+ paymentMethodProcessor.createOrGetExternalPaymentMethod(UUIDs.randomUUID().toString(), account, properties, callContext, internalCallContext) :
+ paymentMethodId;
final String transactionType = TransactionType.PURCHASE.name();
Payment payment = null;
@@ -344,7 +339,7 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
try {
logEnterAPICall(log, transactionType, account, paymentMethodId, paymentId, null, amount, currency, paymentExternalKey, paymentTransactionExternalKey, null, paymentControlPluginNames);
- payment = pluginControlPaymentProcessor.createPurchase(IS_API_PAYMENT, account, nonNullPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
+ payment = pluginControlPaymentProcessor.createPurchase(IS_API_PAYMENT, account, resolvedPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
@@ -607,7 +602,7 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
}
@Override
- public Payment createCreditWithPaymentControl(final Account account, final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency,
+ public Payment createCreditWithPaymentControl(final Account account, @Nullable final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency,
@Nullable final String paymentExternalKey, @Nullable final String paymentTransactionExternalKey,
final Iterable<PluginProperty> properties, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
final List<String> paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions, callContext);
@@ -632,14 +627,11 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
- // TODO validate if the code is located properly here
- // The code should understand that the external payment method needs to be created
- // if it doesn't exist yet and trigger a CREDIT using the (built-in) external payment plugin.
- final UUID nonNullPaymentMethodId = (paymentMethodId != null) ?
- paymentMethodId :
- paymentMethodProcessor.createOrGetExternalPaymentMethod(UUIDs.randomUUID().toString(), account, properties, callContext, internalCallContext);
+ final UUID resolvedPaymentMethodId = (paymentMethodId == null && paymentOptions.isExternalPayment()) ?
+ paymentMethodProcessor.createOrGetExternalPaymentMethod(UUIDs.randomUUID().toString(), account, properties, callContext, internalCallContext) :
+ paymentMethodId;
- payment = pluginControlPaymentProcessor.createCredit(IS_API_PAYMENT, account, nonNullPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
+ payment = pluginControlPaymentProcessor.createCredit(IS_API_PAYMENT, account, resolvedPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
@@ -999,6 +991,17 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
}
@Override
+ public UUID addPaymentMethodWithPaymentControl(final Account account, final String paymentMethodExternalKey, final String pluginName, final boolean setDefault, final PaymentMethodPlugin paymentMethodInfo, final Iterable<PluginProperty> properties, final PaymentOptions paymentOptions, final CallContext context) throws PaymentApiException {
+ final List<String> paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions, context);
+ if (paymentControlPluginNames.isEmpty()) {
+ return addPaymentMethod(account, paymentMethodExternalKey, pluginName, setDefault, paymentMethodInfo, properties, context);
+ }
+
+ return paymentMethodProcessor.addPaymentMethodWithControl(paymentMethodExternalKey, pluginName, account, setDefault, paymentMethodInfo, properties,
+ paymentControlPluginNames, context, internalCallContextFactory.createInternalCallContext(account.getId(), context));
+ }
+
+ @Override
public List<PaymentMethod> getAccountPaymentMethods(final UUID accountId, final boolean includedInactive, final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext context)
throws PaymentApiException {
return paymentMethodProcessor.getPaymentMethods(includedInactive, withPluginInfo, properties, context, internalCallContextFactory.createInternalTenantContext(accountId, context));
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 26bce39..55cddc7 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
@@ -147,6 +147,7 @@ public class DefaultPaymentGatewayApi extends DefaultApiBase implements PaymentG
null,
null,
null,
+ null,
PaymentApiType.HPP,
null,
HPPType.BUILD_FORM_DESCRIPTOR,
@@ -174,6 +175,7 @@ public class DefaultPaymentGatewayApi extends DefaultApiBase implements PaymentG
null,
null,
null,
+ null,
PaymentApiType.HPP,
null,
HPPType.BUILD_FORM_DESCRIPTOR,
@@ -194,6 +196,7 @@ public class DefaultPaymentGatewayApi extends DefaultApiBase implements PaymentG
null,
null,
null,
+ null,
PaymentApiType.HPP,
null,
HPPType.BUILD_FORM_DESCRIPTOR,
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 ff1ee48..5e50070 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,6 +23,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
+import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
@@ -33,12 +34,17 @@ import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
+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.invoice.api.InvoiceInternalApi;
import org.killbill.billing.payment.api.DefaultPaymentMethod;
import org.killbill.billing.payment.api.PaymentApiException;
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.core.sm.control.ControlPluginRunner;
+import org.killbill.billing.payment.core.sm.control.PaymentControlApiAbortException;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.dao.PaymentMethodModelDao;
import org.killbill.billing.payment.dispatcher.PluginDispatcher;
@@ -50,6 +56,7 @@ import org.killbill.billing.payment.provider.DefaultNoOpPaymentMethodPlugin;
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.PluginProperties;
import org.killbill.billing.util.UUIDs;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
@@ -65,6 +72,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
+import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
@@ -80,9 +88,12 @@ import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEn
public class PaymentMethodProcessor extends ProcessorBase {
private static final Logger log = LoggerFactory.getLogger(PaymentMethodProcessor.class);
+ private static final Joiner JOINER = Joiner.on(", ");
private final PluginDispatcher<UUID> uuidPluginNotificationDispatcher;
+ private final ControlPluginRunner controlPluginRunner;
+
private final PaymentConfig paymentConfig;
@Inject
@@ -94,11 +105,13 @@ public class PaymentMethodProcessor extends ProcessorBase {
final GlobalLocker locker,
final PaymentConfig paymentConfig,
final PaymentExecutors executors,
+ final ControlPluginRunner controlPluginRunner,
final InternalCallContextFactory internalCallContextFactory,
final Clock clock) {
super(paymentPluginServiceRegistration, accountInternalApi, paymentDao, tagUserApi, locker, internalCallContextFactory, invoiceApi, clock);
final long paymentPluginTimeoutSec = TimeUnit.SECONDS.convert(paymentConfig.getPaymentPluginTimeout().getPeriod(), paymentConfig.getPaymentPluginTimeout().getUnit());
this.paymentConfig = paymentConfig;
+ this.controlPluginRunner = controlPluginRunner;
this.uuidPluginNotificationDispatcher = new PluginDispatcher<UUID>(paymentPluginTimeoutSec, executors);
}
@@ -145,7 +158,6 @@ public class PaymentMethodProcessor extends ProcessorBase {
return PluginDispatcher.createPluginDispatcherReturnType(pm.getId());
}
-
private void validateUniqueExternalPaymentMethod(final UUID accountId, final String pluginName) throws PaymentApiException {
if (ExternalPaymentProviderPlugin.PLUGIN_NAME.equals(pluginName)) {
final List<PaymentMethodModelDao> accountPaymentMethods = paymentDao.getPaymentMethods(context);
@@ -163,6 +175,119 @@ public class PaymentMethodProcessor extends ProcessorBase {
uuidPluginNotificationDispatcher);
}
+ public UUID addPaymentMethodWithControl(final String paymentMethodExternalKey, final String paymentPluginServiceName, final Account account,
+ final boolean setDefault, final PaymentMethodPlugin paymentMethodProps, final Iterable<PluginProperty> properties,
+ final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext context) throws PaymentApiException {
+ final Iterable<PluginProperty> mergedProperties = PluginProperties.merge(paymentMethodProps.getProperties(), properties);
+ return executeWithPaymentMethodControl(paymentPluginServiceName, account, mergedProperties, paymentControlPluginNames, callContext, uuidPluginNotificationDispatcher, new WithPaymentMethodControlCallback<UUID>() {
+ @Override
+ public UUID doPaymentMethodApiOperation(final String adjustedPaymentPluginServiceName, final Iterable<PluginProperty> adjustedPluginProperties) throws PaymentApiException {
+ if (adjustedPaymentPluginServiceName == null) {
+ return addPaymentMethod(paymentMethodExternalKey, paymentPluginServiceName, account, setDefault, paymentMethodProps, properties, callContext, context);
+ } else {
+ return addPaymentMethod(paymentMethodExternalKey, adjustedPaymentPluginServiceName, account, setDefault, paymentMethodProps, properties, callContext, context);
+ }
+ }
+ });
+ }
+
+
+ private interface WithPaymentMethodControlCallback<T> {
+ T doPaymentMethodApiOperation(final String adjustedPluginName, final Iterable<PluginProperty> adjustedPluginProperties) throws PaymentApiException;
+ }
+
+ private <T> T executeWithPaymentMethodControl(final String paymentPluginServiceName,
+ final Account account,
+ final Iterable<PluginProperty> properties,
+ final List<String> paymentControlPluginNames,
+ final CallContext callContext,
+ final PluginDispatcher<T> pluginDispatcher,
+ final WithPaymentMethodControlCallback<T> callback) throws PaymentApiException {
+
+ return dispatchWithExceptionHandling(account,
+ JOINER.join(paymentControlPluginNames),
+ new Callable<PluginDispatcherReturnType<T>>() {
+ @Override
+ public PluginDispatcherReturnType<T> call() throws Exception {
+ final PriorPaymentControlResult priorCallResult;
+ try {
+ priorCallResult = controlPluginRunner.executePluginPriorCalls(account,
+ null,
+ paymentPluginServiceName,
+ null,
+ null,
+ null,
+ null,
+ null,
+ PaymentApiType.PAYMENT_METHOD,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ true,
+ paymentControlPluginNames,
+ properties,
+ callContext);
+
+ } catch (final PaymentControlApiAbortException e) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_API_ABORTED, e.getPluginName());
+ } catch (final PaymentControlApiException e) {
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_EXCEPTION, e);
+ }
+
+ try {
+ final T result = callback.doPaymentMethodApiOperation(priorCallResult.getAdjustedPluginName(), priorCallResult.getAdjustedPluginProperties());
+ controlPluginRunner.executePluginOnSuccessCalls(account,
+ null,
+ priorCallResult.getAdjustedPluginName(),
+ null,
+ null,
+ null,
+ null,
+ null,
+ PaymentApiType.PAYMENT_METHOD,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ true,
+ paymentControlPluginNames,
+ priorCallResult.getAdjustedPluginProperties(),
+ callContext);
+ return PluginDispatcher.createPluginDispatcherReturnType(result);
+ } catch (final PaymentApiException e) {
+ controlPluginRunner.executePluginOnFailureCalls(account,
+ null,
+ priorCallResult.getAdjustedPluginName(),
+ null,
+ null,
+ null,
+ null,
+ null,
+ PaymentApiType.PAYMENT_METHOD,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ true,
+ paymentControlPluginNames,
+ priorCallResult.getAdjustedPluginProperties(),
+ callContext);
+ throw e;
+ }
+ }
+ },
+ pluginDispatcher);
+ }
+
+
+
private String retrieveActualPaymentMethodExternalKey(final Account account, final PaymentMethod pm, final PaymentPluginApi pluginApi, final Iterable<PluginProperty> properties, final TenantContext callContext, final InternalCallContext context) {
// If the user specified an external key, use it
if (pm.getExternalKey() != null) {
@@ -230,7 +355,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
final PaymentPluginApi pluginApi = getPaymentPluginApi(paymentMethodModelDao.getPluginName());
paymentMethodPlugin = pluginApi.getPaymentMethodDetail(paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId(), properties, tenantContext);
} catch (final PaymentPluginApiException e) {
- throw new PaymentApiException(ErrorCode.PAYMENT_GET_PAYMENT_METHODS, paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId());
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_GET_PAYMENT_METHODS, paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId());
}
} else {
paymentMethodPlugin = null;
@@ -508,8 +633,8 @@ public class PaymentMethodProcessor extends ProcessorBase {
for (final PaymentMethodInfoPlugin cur : pluginPms) {
// If the kbPaymentId is NULL, the plugin does not know about it, so we create a new UUID
final UUID paymentMethodId = cur.getPaymentMethodId() != null ? cur.getPaymentMethodId() : UUIDs.randomUUID();
- // TODO paymentMethod externalKey seems broken here.
- final PaymentMethod input = new DefaultPaymentMethod(paymentMethodId, paymentMethodId.toString(), account.getId(), pluginName);
+ final String externalKey = cur.getExternalPaymentMethodId() != null ? cur.getExternalPaymentMethodId() : paymentMethodId.toString();
+ final PaymentMethod input = new DefaultPaymentMethod(paymentMethodId, externalKey, account.getId(), pluginName);
final PaymentMethodModelDao pmModel = new PaymentMethodModelDao(input.getId(), input.getExternalKey(), input.getCreatedDate(), input.getUpdatedDate(),
input.getAccountId(), input.getPluginName(), input.isActive());
finalPaymentMethods.add(pmModel);
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 8cc235e..caef393 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
@@ -608,7 +608,7 @@ public class PaymentProcessor extends ProcessorBase {
}
}
- Preconditions.checkState(Iterables.<PaymentTransactionModelDao>size(completionCandidates) <= 1, "There should be at most one completion candidate");
+ Preconditions.checkState(Iterables.size(completionCandidates) <= 1, "There should be at most one completion candidate");
return Iterables.<PaymentTransactionModelDao>getLast(completionCandidates, null);
}
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 9ba0ab2..7c503ea 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
@@ -67,6 +67,7 @@ public class CompletionControlOperation extends OperationControlCallback {
final PaymentTransactionModelDao transaction = paymentStateContext.getPaymentTransactionModelDao();
final PaymentControlContext updatedPaymentControlContext = new DefaultPaymentControlContext(paymentStateContext.getAccount(),
paymentStateContext.getPaymentMethodId(),
+ null,
paymentStateControlContext.getAttemptId(),
transaction.getPaymentId(),
paymentStateContext.getPaymentExternalKey(),
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
index c132bea..15da0e2 100644
--- 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
@@ -59,6 +59,7 @@ public class ControlPluginRunner {
public PriorPaymentControlResult executePluginPriorCalls(final Account account,
final UUID paymentMethodId,
+ final String pluginName,
final UUID paymentAttemptId,
final UUID paymentId,
final String paymentExternalKey,
@@ -76,15 +77,17 @@ public class ControlPluginRunner {
final Iterable<PluginProperty> 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);
+ PriorPaymentControlResult prevResult = new DefaultPriorPaymentControlResult(false, amount, currency, paymentMethodId, null, pluginProperties);
// Those values are adjusted prior each call with the result of what previous call to plugin returned
UUID inputPaymentMethodId = paymentMethodId;
+ String inputPaymentMethodName = pluginName;
BigDecimal inputAmount = amount;
Currency inputCurrency = currency;
Iterable<PluginProperty> inputPluginProperties = pluginProperties;
PaymentControlContext inputPaymentControlContext = new DefaultPaymentControlContext(account,
paymentMethodId,
+ pluginName,
paymentAttemptId,
paymentId,
paymentExternalKey,
@@ -100,16 +103,16 @@ public class ControlPluginRunner {
isApiPayment,
callContext);
- for (final String pluginName : paymentControlPluginNames) {
- final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
+ for (final String controlPluginName : paymentControlPluginNames) {
+ final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(controlPluginName);
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);
+ log.warn("Skipping unknown payment control plugin {} when fetching results", controlPluginName);
continue;
}
- log.debug("Calling priorCall of plugin {}", pluginName);
+ log.debug("Calling priorCall of plugin {}", controlPluginName);
prevResult = plugin.priorCall(inputPaymentControlContext, inputPluginProperties);
- log.debug("Successful executed priorCall of plugin {}", pluginName);
+ log.debug("Successful executed priorCall of plugin {}", controlPluginName);
if (prevResult == null) {
// Nothing returned by the plugin
continue;
@@ -118,6 +121,9 @@ public class ControlPluginRunner {
if (prevResult.getAdjustedPaymentMethodId() != null) {
inputPaymentMethodId = prevResult.getAdjustedPaymentMethodId();
}
+ if (prevResult.getAdjustedPluginName() != null) {
+ inputPaymentMethodName = prevResult.getAdjustedPluginName();
+ }
if (prevResult.getAdjustedAmount() != null) {
inputAmount = prevResult.getAdjustedAmount();
}
@@ -128,10 +134,11 @@ public class ControlPluginRunner {
inputPluginProperties = prevResult.getAdjustedPluginProperties();
}
if (prevResult.isAborted()) {
- throw new PaymentControlApiAbortException(pluginName);
+ throw new PaymentControlApiAbortException(controlPluginName);
}
inputPaymentControlContext = new DefaultPaymentControlContext(account,
inputPaymentMethodId,
+ controlPluginName,
paymentAttemptId,
paymentId,
paymentExternalKey,
@@ -148,12 +155,13 @@ public class ControlPluginRunner {
callContext);
}
// Rebuild latest result to include inputPluginProperties
- prevResult = new DefaultPriorPaymentControlResult(prevResult != null && prevResult.isAborted(), inputPaymentMethodId, inputAmount, inputCurrency, inputPluginProperties);
+ prevResult = new DefaultPriorPaymentControlResult(prevResult != null && prevResult.isAborted(), inputPaymentMethodId, inputPaymentMethodName, inputAmount, inputCurrency, inputPluginProperties);
return prevResult;
}
public OnSuccessPaymentControlResult executePluginOnSuccessCalls(final Account account,
final UUID paymentMethodId,
+ final String pluginName,
final UUID paymentAttemptId,
final UUID paymentId,
final String paymentExternalKey,
@@ -173,6 +181,7 @@ public class ControlPluginRunner {
final PaymentControlContext inputPaymentControlContext = new DefaultPaymentControlContext(account,
paymentMethodId,
+ pluginName,
paymentAttemptId,
paymentId,
paymentExternalKey,
@@ -189,13 +198,13 @@ public class ControlPluginRunner {
callContext);
Iterable<PluginProperty> inputPluginProperties = pluginProperties;
- for (final String pluginName : paymentControlPluginNames) {
- final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
+ for (final String controlPluginName : paymentControlPluginNames) {
+ final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(controlPluginName);
if (plugin != null) {
try {
- log.debug("Calling onSuccessCall of plugin {}", pluginName);
+ log.debug("Calling onSuccessCall of plugin {}", controlPluginName);
final OnSuccessPaymentControlResult result = plugin.onSuccessCall(inputPaymentControlContext, inputPluginProperties);
- log.debug("Successful executed onSuccessCall of plugin {}", pluginName);
+ log.debug("Successful executed onSuccessCall of plugin {}", controlPluginName);
if (result == null) {
// Nothing returned by the plugin
continue;
@@ -206,9 +215,9 @@ public class ControlPluginRunner {
}
// Exceptions from the control plugins are ignored (and logged) because the semantics on what to do are undefined.
} catch (final PaymentControlApiException e) {
- log.warn("Error during onSuccessCall for plugin='{}', paymentExternalKey='{}'", pluginName, inputPaymentControlContext.getPaymentExternalKey(), e);
+ log.warn("Error during onSuccessCall for plugin='{}', paymentExternalKey='{}'", controlPluginName, inputPaymentControlContext.getPaymentExternalKey(), e);
} catch (final RuntimeException e) {
- log.warn("Error during onSuccessCall for plugin='{}', paymentExternalKey='{}'", pluginName, inputPaymentControlContext.getPaymentExternalKey(), e);
+ log.warn("Error during onSuccessCall for plugin='{}', paymentExternalKey='{}'", controlPluginName, inputPaymentControlContext.getPaymentExternalKey(), e);
}
}
}
@@ -217,6 +226,7 @@ public class ControlPluginRunner {
public OnFailurePaymentControlResult executePluginOnFailureCalls(final Account account,
final UUID paymentMethodId,
+ final String pluginName,
final UUID paymentAttemptId,
final UUID paymentId,
final String paymentExternalKey,
@@ -236,6 +246,7 @@ public class ControlPluginRunner {
final PaymentControlContext inputPaymentControlContext = new DefaultPaymentControlContext(account,
paymentMethodId,
+ pluginName,
paymentAttemptId,
paymentId,
paymentExternalKey,
@@ -254,13 +265,13 @@ public class ControlPluginRunner {
DateTime candidate = null;
Iterable<PluginProperty> inputPluginProperties = pluginProperties;
- for (final String pluginName : paymentControlPluginNames) {
- final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
+ for (final String controlPluginName : paymentControlPluginNames) {
+ final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(controlPluginName);
if (plugin != null) {
try {
- log.debug("Calling onSuccessCall of plugin {}", pluginName);
+ log.debug("Calling onSuccessCall of plugin {}", controlPluginName);
final OnFailurePaymentControlResult result = plugin.onFailureCall(inputPaymentControlContext, inputPluginProperties);
- log.debug("Successful executed onSuccessCall of plugin {}", pluginName);
+ log.debug("Successful executed onSuccessCall of plugin {}", controlPluginName);
if (result == null) {
// Nothing returned by the plugin
continue;
@@ -277,7 +288,7 @@ public class ControlPluginRunner {
}
} catch (final PaymentControlApiException e) {
- log.warn("Error during onFailureCall for plugin='{}', paymentExternalKey='{}'", pluginName, inputPaymentControlContext.getPaymentExternalKey(), e);
+ log.warn("Error during onFailureCall for plugin='{}', paymentExternalKey='{}'", controlPluginName, inputPaymentControlContext.getPaymentExternalKey(), e);
return new DefaultFailureCallResult(candidate, inputPluginProperties);
}
}
@@ -289,6 +300,7 @@ public class ControlPluginRunner {
private final Account account;
private final UUID paymentMethodId;
+ private final String pluginName;
private final UUID attemptId;
private final UUID paymentId;
private final String paymentExternalKey;
@@ -305,6 +317,7 @@ public class ControlPluginRunner {
public DefaultPaymentControlContext(final Account account,
final UUID paymentMethodId,
+ @Nullable String pluginName,
final UUID attemptId,
@Nullable final UUID paymentId,
final String paymentExternalKey,
@@ -322,6 +335,7 @@ public class ControlPluginRunner {
super(account.getId(), 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.pluginName = pluginName;
this.attemptId = attemptId;
this.paymentId = paymentId;
this.paymentExternalKey = paymentExternalKey;
@@ -383,6 +397,11 @@ public class ControlPluginRunner {
}
@Override
+ public String getPaymentPluginName() {
+ return pluginName;
+ }
+
+ @Override
public UUID getPaymentId() {
return paymentId;
}
@@ -416,6 +435,7 @@ public class ControlPluginRunner {
return "DefaultPaymentControlContext{" +
"account=" + account +
", paymentMethodId=" + paymentMethodId +
+ ", pluginName=" + pluginName +
", attemptId=" + attemptId +
", paymentId=" + paymentId +
", paymentExternalKey='" + paymentExternalKey + '\'' +
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 a562e32..65348af 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
@@ -88,6 +88,7 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
final PaymentControlContext paymentControlContext = new DefaultPaymentControlContext(paymentStateContext.getAccount(),
paymentStateContext.getPaymentMethodId(),
+ null,
paymentStateControlContext.getAttemptId(),
paymentStateContext.getPaymentId(),
paymentStateContext.getPaymentExternalKey(),
@@ -123,6 +124,7 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
success = transaction.getTransactionStatus() == TransactionStatus.SUCCESS || transaction.getTransactionStatus() == TransactionStatus.PENDING;
final PaymentControlContext updatedPaymentControlContext = new DefaultPaymentControlContext(paymentStateContext.getAccount(),
paymentStateContext.getPaymentMethodId(),
+ null,
paymentStateControlContext.getAttemptId(),
result.getId(),
result.getExternalKey(),
@@ -173,6 +175,7 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
final PriorPaymentControlResult result = controlPluginRunner.executePluginPriorCalls(paymentStateContext.getAccount(),
paymentControlContextArg.getPaymentMethodId(),
+ null,
paymentStateControlContext.getAttemptId(),
paymentStateContext.getPaymentId(),
paymentStateContext.getPaymentExternalKey(),
@@ -199,6 +202,7 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
// 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(),
+ null,
paymentStateControlContext.getAttemptId(),
paymentControlContext.getPaymentId(),
paymentControlContext.getPaymentExternalKey(),
@@ -230,6 +234,7 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
final OnFailurePaymentControlResult result = controlPluginRunner.executePluginOnFailureCalls(paymentStateContext.getAccount(),
paymentControlContext.getPaymentMethodId(),
+ null,
paymentStateControlContext.getAttemptId(),
paymentControlContext.getPaymentId(),
paymentControlContext.getPaymentExternalKey(),
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java
index 5e07f04..5f03f39 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java
@@ -50,7 +50,7 @@ public class ChargebackInitiated extends PaymentLeavingStateCallback {
ImmutableList.<PaymentTransactionModelDao>of();
final Iterable<PaymentTransactionModelDao> existingPaymentTransactionsForTransactionIdOrKey = filterExistingPaymentTransactionsForTransactionIdOrKey(paymentTransactionsForCurrentPayment, paymentStateContext.getTransactionId(), paymentStateContext.getPaymentTransactionExternalKey());
- if (Iterables.<PaymentTransactionModelDao>isEmpty(existingPaymentTransactionsForTransactionIdOrKey)) {
+ if (Iterables.isEmpty(existingPaymentTransactionsForTransactionIdOrKey)) {
// Chargeback reversals can only happen after a successful chargeback
throw new OperationException(new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_SUCCESS_PAYMENT, paymentStateContext.getPaymentId()));
}
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 f03d42f..6aa0bd6 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
@@ -321,11 +321,16 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
return new DefaultPriorPaymentControlResult(true);
}
+
// Get account and check if it is child and payment is delegated to parent => abort
+
final AccountData accountData = accountApi.getAccountById(invoice.getAccountId(), internalContext);
- if ((accountData != null) && (accountData.getParentAccountId() != null) && accountData.isPaymentDelegatedToParent()) {
+ if (((accountData != null) && (accountData.getParentAccountId() != null) && accountData.isPaymentDelegatedToParent()) || // Valid when we initially create the child invoice (even if parent invoice does not exist yet)
+ (invoice.getParentAccountId() != null)) { // Valid after we have unparented the child
return new DefaultPriorPaymentControlResult(true);
}
+
+
final BigDecimal requestedAmount = validateAndComputePaymentAmount(invoice, paymentControlPluginContext.getAmount(), paymentControlPluginContext.isApiPayment());
final boolean isAborted = requestedAmount.compareTo(BigDecimal.ZERO) == 0;
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultPriorPaymentControlResult.java b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultPriorPaymentControlResult.java
index 27ce65b..4ab3d4b 100644
--- a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultPriorPaymentControlResult.java
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultPriorPaymentControlResult.java
@@ -30,22 +30,25 @@ public class DefaultPriorPaymentControlResult implements PriorPaymentControlResu
private final BigDecimal adjustedRetryAmount;
private final Currency adjustedCurrency;
private final UUID adjustedPaymentMethodId;
+ private final String adjustedPaymentPluginName;
private final Iterable<PluginProperty> adjustedPluginProperties;
public DefaultPriorPaymentControlResult(final boolean isAborted,
final BigDecimal adjustedRetryAmount,
final Currency adjustedCurrency,
final UUID adjustedPaymentMethodId,
+ final String adjustedPaymentPluginName,
final Iterable<PluginProperty> adjustedPluginProperties) {
this.isAborted = isAborted;
this.adjustedRetryAmount = adjustedRetryAmount;
this.adjustedCurrency = adjustedCurrency;
this.adjustedPaymentMethodId = adjustedPaymentMethodId;
this.adjustedPluginProperties = adjustedPluginProperties;
+ this.adjustedPaymentPluginName = adjustedPaymentPluginName;
}
public DefaultPriorPaymentControlResult(final boolean isAborted, final BigDecimal adjustedRetryAmount) {
- this(isAborted, adjustedRetryAmount, null, null, null);
+ this(isAborted, adjustedRetryAmount, null, null, null, null);
}
public DefaultPriorPaymentControlResult(final boolean isAborted) {
@@ -53,8 +56,8 @@ public class DefaultPriorPaymentControlResult implements PriorPaymentControlResu
}
- public DefaultPriorPaymentControlResult(final boolean isAborted, final UUID adjustedPaymentMethodId, final BigDecimal adjustedAmount, final Currency adjustedCurrency, final Iterable<PluginProperty> adjustedPluginProperties) {
- this(isAborted, adjustedAmount, adjustedCurrency, adjustedPaymentMethodId, adjustedPluginProperties);
+ public DefaultPriorPaymentControlResult(final boolean isAborted, final UUID adjustedPaymentMethodId, final String adjustedPaymentPluginName, final BigDecimal adjustedAmount, final Currency adjustedCurrency, final Iterable<PluginProperty> adjustedPluginProperties) {
+ this(isAborted, adjustedAmount, adjustedCurrency, adjustedPaymentMethodId, adjustedPaymentPluginName, adjustedPluginProperties);
}
@Override
@@ -78,6 +81,11 @@ public class DefaultPriorPaymentControlResult implements PriorPaymentControlResu
}
@Override
+ public String getAdjustedPluginName() {
+ return adjustedPaymentPluginName;
+ }
+
+ @Override
public Iterable<PluginProperty> getAdjustedPluginProperties() {
return adjustedPluginProperties;
}
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 545fcc4..22fd0b3 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
@@ -645,6 +645,28 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Currency.USD);
}
+ @Test(groups = "slow")
+ public void testAddPaymentMethodWithControl() throws PaymentApiException {
+ final PaymentMethodPlugin paymentMethodInfo = new DefaultNoOpPaymentMethodPlugin(UUID.randomUUID().toString(), false, null);
+ testPaymentControlPluginApi.setNewPaymentMethodName(MockPaymentProviderPlugin.PLUGIN_NAME);
+ final UUID newPaymentMethodId = paymentApi.addPaymentMethodWithPaymentControl(account, null, "SomeDummyValueToBeChanged", false, paymentMethodInfo, ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+
+
+ final PaymentMethod paymentMethod = paymentApi.getPaymentMethodById(newPaymentMethodId, false, false, ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(paymentMethod.getPluginName(), MockPaymentProviderPlugin.PLUGIN_NAME);
+
+ final Payment payment = paymentApi.createAuthorizationWithPaymentControl(account, newPaymentMethodId, null, BigDecimal.TEN, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getPaymentMethodId(), newPaymentMethodId);
+
+ verifyOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ BigDecimal.TEN,
+ Currency.USD);
+ }
+
private void verifyPriorAndOnSuccess(final UUID paymentId,
final String paymentExternalKey,
final UUID paymentTransactionId,
@@ -830,6 +852,7 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
public static final String PLUGIN_NAME = "TEST_CONTROL_API_PLUGIN_NAME";
private UUID newPaymentMethodId;
+ private String newPaymentMethodName;
private UUID actualPriorCallPaymentId;
private String actualPriorCallPaymentExternalKey;
@@ -856,6 +879,10 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
this.newPaymentMethodId = newPaymentMethodId;
}
+ public void setNewPaymentMethodName(final String newPaymentMethodName) {
+ this.newPaymentMethodName = newPaymentMethodName;
+ }
+
public UUID getActualPriorCallPaymentId() {
return actualPriorCallPaymentId;
}
@@ -959,6 +986,11 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
}
@Override
+ public String getAdjustedPluginName() {
+ return newPaymentMethodName;
+ }
+
+ @Override
public Iterable<PluginProperty> getAdjustedPluginProperties() {
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
index 3aa3554..4698e0d 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentGatewayApiWithPaymentControl.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentGatewayApiWithPaymentControl.java
@@ -257,7 +257,7 @@ public class TestPaymentGatewayApiWithPaymentControl extends PaymentTestSuiteNoD
@Override
public PriorPaymentControlResult priorCall(final PaymentControlContext paymentControlContext, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
- return new DefaultPriorPaymentControlResult(aborted, account.getPaymentMethodId(), null, null, getAdjustedProperties(properties, newPriorCallProperties, removedPriorCallProperties));
+ return new DefaultPriorPaymentControlResult(aborted, account.getPaymentMethodId(), null, null, null, getAdjustedProperties(properties, newPriorCallProperties, removedPriorCallProperties));
}
@Override
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java b/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
index 8dc40d7..432bd72 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
@@ -78,7 +78,7 @@ public class TestIncompletePaymentTransactionTaskWithDB extends PaymentTestSuite
final JanitorNotificationKey notificationKey = new JanitorNotificationKey(transactionId, incompletePaymentTransactionTask.getClass().toString(), 1);
final UUID userToken = UUID.randomUUID();
- Assert.assertTrue(Iterables.<NotificationEventWithMetadata<NotificationEvent>>isEmpty(incompletePaymentTransactionTask.janitorQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId())));
+ Assert.assertTrue(Iterables.isEmpty(incompletePaymentTransactionTask.janitorQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId())));
GlobalLock lock = null;
try {
@@ -87,7 +87,7 @@ public class TestIncompletePaymentTransactionTaskWithDB extends PaymentTestSuite
incompletePaymentTransactionTask.processNotification(notificationKey, userToken, internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
final Iterable<NotificationEventWithMetadata<NotificationEvent>> futureNotifications = incompletePaymentTransactionTask.janitorQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
- Assert.assertFalse(Iterables.<NotificationEventWithMetadata<NotificationEvent>>isEmpty(futureNotifications));
+ Assert.assertFalse(Iterables.isEmpty(futureNotifications));
final NotificationEventWithMetadata<NotificationEvent> notificationEventWithMetadata = ImmutableList.<NotificationEventWithMetadata<NotificationEvent>>copyOf(futureNotifications).get(0);
Assert.assertEquals(notificationEventWithMetadata.getUserToken(), userToken);
Assert.assertEquals(notificationEventWithMetadata.getEvent().getClass(), JanitorNotificationKey.class);
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
index 80cb640..1e114e4 100644
--- 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
@@ -54,6 +54,7 @@ public class TestControlPluginRunner extends PaymentTestSuiteNoDB {
final PriorPaymentControlResult paymentControlResult = controlPluginRunner.executePluginPriorCalls(account,
paymentMethodId,
null,
+ null,
paymentId,
paymentExternalKey,
paymentTransactionId,
diff --git a/payment/src/test/java/org/killbill/billing/payment/MockInvoice.java b/payment/src/test/java/org/killbill/billing/payment/MockInvoice.java
index 0100b74..eca259b 100644
--- a/payment/src/test/java/org/killbill/billing/payment/MockInvoice.java
+++ b/payment/src/test/java/org/killbill/billing/payment/MockInvoice.java
@@ -222,5 +222,15 @@ public class MockInvoice extends EntityBase implements Invoice {
public boolean isParentInvoice() {
return parentInvoice;
}
+
+ @Override
+ public UUID getParentAccountId() {
+ return null;
+ }
+
+ @Override
+ public UUID getParentInvoiceId() {
+ return null;
+ }
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentControlProviderPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentControlProviderPlugin.java
index f451943..f7c949e 100644
--- a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentControlProviderPlugin.java
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentControlProviderPlugin.java
@@ -77,7 +77,7 @@ public class MockPaymentControlProviderPlugin implements PaymentControlPluginApi
} else if (exception instanceof RuntimeException) {
throw (RuntimeException) exception;
}
- return new DefaultPriorPaymentControlResult(isAborted, adjustedPaymentMethodId, null, null, null);
+ return new DefaultPriorPaymentControlResult(isAborted, adjustedPaymentMethodId, null, null, null, null);
}
@Override
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 73d4bbd..a124e0b 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
@@ -525,7 +525,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
private int getPendingNotificationCnt(final InternalCallContext internalCallContext) {
try {
- return Iterables.<NotificationEventWithMetadata>size(notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, Janitor.QUEUE_NAME).getFutureOrInProcessingNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId()));
+ return Iterables.size(notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, Janitor.QUEUE_NAME).getFutureOrInProcessingNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId()));
} catch (final Exception e) {
fail("Test failed ", e);
}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/LoggingFilterObfuscator.java b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/LoggingFilterObfuscator.java
new file mode 100644
index 0000000..536682f
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/LoggingFilterObfuscator.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * 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.log.obfuscators;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.regex.Pattern;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import com.google.common.collect.ImmutableList;
+
+public class LoggingFilterObfuscator extends Obfuscator {
+
+ private static final String[] DEFAULT_SENSITIVE_HEADERS = {
+ "Authorization",
+ "X-Killbill-ApiSecret",
+ };
+
+ private final Collection<Pattern> patterns = new LinkedList<Pattern>();
+
+ public LoggingFilterObfuscator() {
+ this(ImmutableList.<Pattern>of());
+ }
+
+ public LoggingFilterObfuscator(final Collection<Pattern> extraPatterns) {
+ super();
+
+ for (final String sensitiveKey : DEFAULT_SENSITIVE_HEADERS) {
+ this.patterns.add(buildPattern(sensitiveKey));
+ }
+ this.patterns.addAll(extraPatterns);
+ }
+
+ @Override
+ public String obfuscate(final String originalString, final ILoggingEvent event) {
+ return obfuscate(originalString, patterns, event);
+ }
+
+ private Pattern buildPattern(final String key) {
+ return Pattern.compile("\\s*" + key + ":\\s*([^\\n]+)", DEFAULT_PATTERN_FLAGS);
+ }
+}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java
index 39b9306..6e522d6 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java
@@ -45,6 +45,7 @@ import com.google.common.collect.ImmutableList;
public class ObfuscatorConverter extends ClassicConverter {
private final Collection<Obfuscator> obfuscators = ImmutableList.<Obfuscator>of(new ConfigMagicObfuscator(),
+ new LoggingFilterObfuscator(),
new PatternObfuscator(),
new LuhnMaskingObfuscator());
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 5c74bcb..92668d3 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
@@ -31,7 +31,6 @@ import org.killbill.billing.jaxrs.resources.AccountResource;
import org.killbill.billing.jaxrs.resources.AdminResource;
import org.killbill.billing.jaxrs.resources.BundleResource;
import org.killbill.billing.jaxrs.resources.CatalogResource;
-import org.killbill.billing.jaxrs.resources.ComboPaymentResource;
import org.killbill.billing.jaxrs.resources.CreditResource;
import org.killbill.billing.jaxrs.resources.CustomFieldResource;
import org.killbill.billing.jaxrs.resources.ExportResource;
@@ -68,7 +67,6 @@ import org.killbill.billing.subscription.glue.DefaultSubscriptionModule;
import org.killbill.billing.tenant.glue.DefaultTenantModule;
import org.killbill.billing.usage.glue.UsageModule;
import org.killbill.billing.util.config.definition.NotificationConfig;
-import org.killbill.billing.util.email.EmailModule;
import org.killbill.billing.util.email.templates.TemplateModule;
import org.killbill.billing.util.glue.AuditModule;
import org.killbill.billing.util.glue.BroadcastModule;
@@ -170,7 +168,6 @@ public class KillbillServerModule extends KillbillPlatformModule {
install(new DefaultJunctionModule(configSource));
install(new DefaultOverdueModule(configSource));
install(new DefaultSubscriptionModule(configSource));
- install(new EmailModule(configSource));
install(new ExportModule(configSource));
install(new GlobalLockerModule(configSource));
install(new KillBillShiroAopModule());
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
index 49057fb..eaf9e55 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
@@ -44,6 +44,7 @@ import org.killbill.billing.util.glue.KillBillShiroModule;
import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
import org.killbill.billing.util.security.shiro.realm.KillBillJdbcRealm;
import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
+import org.killbill.billing.util.security.shiro.realm.KillBillOktaRealm;
import org.skife.config.ConfigSource;
import org.skife.config.ConfigurationObjectFactory;
@@ -87,6 +88,9 @@ public class KillBillShiroWebModule extends ShiroWebModuleWith435 {
if (KillBillShiroModule.isLDAPEnabled()) {
bindRealm().to(KillBillJndiLdapRealm.class).asEagerSingleton();
}
+ if (KillBillShiroModule.isOktaEnabled()) {
+ bindRealm().to(KillBillOktaRealm.class).asEagerSingleton();
+ }
bindListener(new AbstractMatcher<TypeLiteral<?>>() {
@Override
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
index 4205e8a..a0f6ad4 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
@@ -107,7 +107,7 @@ public class TestCatalog extends TestJaxrsBase {
Assert.assertEquals(catalogsJson.get(0).getEffectiveDate(), Date.valueOf("2011-01-01"));
Assert.assertEquals(catalogsJson.get(0).getCurrencies().size(), 3);
Assert.assertEquals(catalogsJson.get(0).getProducts().size(), 11);
- Assert.assertEquals(catalogsJson.get(0).getPriceLists().size(), 6);
+ Assert.assertEquals(catalogsJson.get(0).getPriceLists().size(), 7);
for (final Product productJson : catalogsJson.get(0).getProducts()) {
if (!"BASE".equals(productJson.getType())) {
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCustomField.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCustomField.java
index 66a3af7..123bb59 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCustomField.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCustomField.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2014 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
@@ -27,6 +27,7 @@ import org.killbill.billing.client.KillBillClientException;
import org.killbill.billing.client.model.Account;
import org.killbill.billing.client.model.CustomField;
import org.killbill.billing.client.model.CustomFields;
+import org.killbill.billing.util.api.AuditLevel;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -39,13 +40,13 @@ public class TestCustomField extends TestJaxrsBase {
final CustomField customField = new CustomField();
customField.setName(UUID.randomUUID().toString().substring(0, 5));
customField.setValue(UUID.randomUUID().toString().substring(0, 5));
- killBillClient.createAccountCustomField(account.getAccountId(), customField, createdBy, reason, comment);
+ killBillClient.createAccountCustomField(account.getAccountId(), customField, requestOptions);
}
- final CustomFields allCustomFields = killBillClient.getCustomFields();
+ final CustomFields allCustomFields = killBillClient.getCustomFields(requestOptions);
Assert.assertEquals(allCustomFields.size(), 5);
- CustomFields page = killBillClient.getCustomFields(0L, 1L);
+ CustomFields page = killBillClient.getCustomFields(0L, 1L, requestOptions);
for (int i = 0; i < 5; i++) {
Assert.assertNotNull(page);
Assert.assertEquals(page.size(), 1);
@@ -60,15 +61,21 @@ public class TestCustomField extends TestJaxrsBase {
doSearchCustomField(customField.getValue(), customField);
}
- final CustomFields customFields = killBillClient.searchCustomFields(ObjectType.ACCOUNT.toString());
+ final CustomFields customFields = killBillClient.searchCustomFields(ObjectType.ACCOUNT.toString(), requestOptions);
Assert.assertEquals(customFields.size(), 5);
Assert.assertEquals(customFields.getPaginationCurrentOffset(), 0);
Assert.assertEquals(customFields.getPaginationTotalNbRecords(), 5);
Assert.assertEquals(customFields.getPaginationMaxNbRecords(), 5);
+
+ final CustomFields allAccountCustomFields = killBillClient.getAllAccountCustomFields(account.getAccountId(), null, AuditLevel.FULL, requestOptions);
+ Assert.assertEquals(allAccountCustomFields.size(), 5);
+
+ final CustomFields allBundleCustomFieldsForAccount = killBillClient.getAllAccountCustomFields(account.getAccountId(), ObjectType.ACCOUNT.name(), AuditLevel.FULL, requestOptions);
+ Assert.assertEquals(allBundleCustomFieldsForAccount.size(), 5);
}
private void doSearchCustomField(final String searchKey, @Nullable final CustomField expectedCustomField) throws KillBillClientException {
- final CustomFields customFields = killBillClient.searchCustomFields(searchKey);
+ final CustomFields customFields = killBillClient.searchCustomFields(searchKey, requestOptions);
if (expectedCustomField == null) {
Assert.assertTrue(customFields.isEmpty());
Assert.assertEquals(customFields.getPaginationCurrentOffset(), 0);
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 3c5cf56..9b8e81d 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
@@ -598,98 +598,6 @@ public class TestInvoice extends TestJaxrsBase {
assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions).size(), 3);
}
- @Test(groups = "slow", description = "Can create an external charge on an existing invoice")
- public void testExternalChargeOnExistingInvoice() throws Exception {
- final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
-
- // Get the invoices
- final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, false, requestOptions);
- // 2 invoices but look for the non zero dollar one
- assertEquals(invoices.size(), 2);
- final UUID invoiceId = invoices.get(1).getInvoiceId();
- final BigDecimal originalInvoiceAmount = invoices.get(1).getAmount();
- final int originalNumberOfItemsForInvoice = invoices.get(1).getItems().size();
-
- // Post an external charge
- final BigDecimal chargeAmount = BigDecimal.TEN;
- final InvoiceItem externalCharge = new InvoiceItem();
- externalCharge.setAccountId(accountJson.getAccountId());
- externalCharge.setAmount(chargeAmount);
- externalCharge.setCurrency(accountJson.getCurrency());
- externalCharge.setInvoiceId(invoiceId);
- final List<InvoiceItem> createdExternalCharges = killBillClient.createExternalCharges(ImmutableList.of(externalCharge), clock.getUTCToday(), false, true, requestOptions);
- assertEquals(createdExternalCharges.size(), 1);
- final Invoice invoiceWithItems = killBillClient.getInvoice(createdExternalCharges.get(0).getInvoiceId(), true, false, requestOptions);
- assertEquals(invoiceWithItems.getItems().size(), originalNumberOfItemsForInvoice + 1);
- assertNull(invoiceWithItems.getItems().get(originalNumberOfItemsForInvoice).getBundleId());
-
- // Verify the new invoice balance
- final Invoice adjustedInvoice = killBillClient.getInvoice(invoiceId, requestOptions);
- final BigDecimal adjustedInvoiceBalance = originalInvoiceAmount.add(chargeAmount.setScale(2, RoundingMode.HALF_UP));
- assertEquals(adjustedInvoice.getBalance().compareTo(adjustedInvoiceBalance), 0);
- }
-
- @Test(groups = "slow", description = "Can create an external charge on an existing invoice and trigger a payment")
- public void testExternalChargeOnExistingInvoiceWithAutomaticPayment() throws Exception {
- final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
-
- // Get the invoices
- final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, false, requestOptions);
- // 2 invoices but look for the non zero dollar one
- assertEquals(invoices.size(), 2);
- final UUID invoiceId = invoices.get(1).getInvoiceId();
- final int originalNumberOfItemsForInvoice = invoices.get(1).getItems().size();
-
- // Post an external charge
- final BigDecimal chargeAmount = BigDecimal.TEN;
- final InvoiceItem externalCharge = new InvoiceItem();
- externalCharge.setAccountId(accountJson.getAccountId());
- externalCharge.setAmount(chargeAmount);
- externalCharge.setCurrency(accountJson.getCurrency());
- externalCharge.setInvoiceId(invoiceId);
- final List<InvoiceItem> createdExternalCharges = killBillClient.createExternalCharges(ImmutableList.of(externalCharge), clock.getUTCToday(), true, true, requestOptions);
- assertEquals(createdExternalCharges.size(), 1);
- final Invoice invoiceWithItems = killBillClient.getInvoice(createdExternalCharges.get(0).getInvoiceId(), true,false, requestOptions);
- assertEquals(invoiceWithItems.getItems().size(), originalNumberOfItemsForInvoice + 1);
- assertNull(invoiceWithItems.getItems().get(originalNumberOfItemsForInvoice).getBundleId());
-
- // Verify the new invoice balance
- final Invoice adjustedInvoice = killBillClient.getInvoice(invoiceId, requestOptions);
- assertEquals(adjustedInvoice.getBalance().compareTo(BigDecimal.ZERO), 0);
- }
-
- @Test(groups = "slow", description = "Can create an external charge for a bundle on an existing invoice")
- public void testExternalChargeForBundleOnExistingInvoice() throws Exception {
- final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
-
- // Get the invoices
- final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, false, requestOptions);
- // 2 invoices but look for the non zero dollar one
- assertEquals(invoices.size(), 2);
- final UUID invoiceId = invoices.get(1).getInvoiceId();
- final BigDecimal originalInvoiceAmount = invoices.get(1).getAmount();
- final int originalNumberOfItemsForInvoice = invoices.get(1).getItems().size();
-
- // Post an external charge
- final BigDecimal chargeAmount = BigDecimal.TEN;
- final UUID bundleId = UUID.randomUUID();
- final InvoiceItem externalCharge = new InvoiceItem();
- externalCharge.setAccountId(accountJson.getAccountId());
- externalCharge.setAmount(chargeAmount);
- externalCharge.setCurrency(accountJson.getCurrency());
- externalCharge.setInvoiceId(invoiceId);
- externalCharge.setBundleId(bundleId);
- final List<InvoiceItem> createdExternalCharges = killBillClient.createExternalCharges(ImmutableList.<InvoiceItem>of(externalCharge), clock.getUTCToday(), false, true, requestOptions);
- final Invoice invoiceWithItems = killBillClient.getInvoice(createdExternalCharges.get(0).getInvoiceId(), true,false, requestOptions);
- assertEquals(invoiceWithItems.getItems().size(), originalNumberOfItemsForInvoice + 1);
- assertEquals(invoiceWithItems.getItems().get(originalNumberOfItemsForInvoice).getBundleId(), bundleId);
-
- // Verify the new invoice balance
- final Invoice adjustedInvoice = killBillClient.getInvoice(invoiceId, requestOptions);
- final BigDecimal adjustedInvoiceBalance = originalInvoiceAmount.add(chargeAmount.setScale(2, RoundingMode.HALF_UP));
- assertEquals(adjustedInvoice.getBalance().compareTo(adjustedInvoiceBalance), 0);
- }
-
@Test(groups = "slow", description = "Can paginate and search through all invoices")
public void testInvoicesPagination() throws Exception {
createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
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 66007ef..0488304 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
@@ -37,13 +37,10 @@ import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
import org.killbill.billing.api.TestApiListener;
import org.killbill.billing.client.KillBillClient;
import org.killbill.billing.client.KillBillHttpClient;
-import org.killbill.billing.client.RequestOptions;
import org.killbill.billing.client.model.Payment;
import org.killbill.billing.client.model.PaymentTransaction;
import org.killbill.billing.client.model.Tenant;
-import org.killbill.billing.invoice.api.InvoiceNotifier;
import org.killbill.billing.invoice.glue.DefaultInvoiceModule;
-import org.killbill.billing.invoice.notification.NullInvoiceNotifier;
import org.killbill.billing.jetty.HttpServer;
import org.killbill.billing.jetty.HttpServerConfig;
import org.killbill.billing.lifecycle.glue.BusModule;
@@ -145,10 +142,6 @@ public class TestJaxrsBase extends KillbillClient {
super(configSource);
}
- @Override
- protected void installInvoiceNotifier() {
- bind(InvoiceNotifier.class).to(NullInvoiceNotifier.class).asEagerSingleton();
- }
}
private final class PaymentMockModule extends PaymentModule {
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 44a3f3d..5ba7ba8 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
@@ -79,9 +79,6 @@ public class TestTag extends TestJaxrsBase {
List<TagDefinition> objFromJson = killBillClient.getTagDefinitions(requestOptions);
final int sizeSystemTag = objFromJson.isEmpty() ? 0 : objFromJson.size();
- for (final TagDefinition cur : objFromJson) {
- Assert.assertFalse(SystemTags.isSystemTag(cur.getId()));
- }
final TagDefinition inputBlue = new TagDefinition(null, false, "blue", "relaxing color", ImmutableList.<ObjectType>of());
killBillClient.createTagDefinition(inputBlue, requestOptions);
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestLoggingFilterObfuscator.java b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestLoggingFilterObfuscator.java
new file mode 100644
index 0000000..76351fd
--- /dev/null
+++ b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestLoggingFilterObfuscator.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * 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.log.obfuscators;
+
+import org.killbill.billing.server.log.ServerTestSuiteNoDB;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+
+public class TestLoggingFilterObfuscator extends ServerTestSuiteNoDB {
+
+ private final LoggingFilterObfuscator obfuscator = new LoggingFilterObfuscator();
+
+ @Test(groups = "fast")
+ public void testAuthorization() throws Exception {
+ verify("2017-08-26T10:28:21,959+0000 lvl='INFO', log='LoggingFilter', th='qtp1071550332-34', xff='', rId='70394abe-7ab6-4b7c-aaf5-17abfcdb9622', aRId='', tRId='', 1 * Server in-bound request\n" +
+ "1 > GET http://127.0.0.1:8080/1.0/kb/security/permissions\n" +
+ "1 > User-Agent: killbill/1.9.0; jruby 9.1.12.0 (2.3.3) 2017-06-15 33c6439 Java HotSpot(TM) 64-Bit Server VM 25.121-b13 on 1.8.0_121-b13 +jit [darwin-x86_64]\n" +
+ "1 > Authorization: Basic YWRtaW46cGFzc3dvcmQ=\n" +
+ "1 > Host: 127.0.0.1:8080\n" +
+ "1 > Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\n" +
+ "1 > Accept: application/json\n" +
+ "1 >",
+ "2017-08-26T10:28:21,959+0000 lvl='INFO', log='LoggingFilter', th='qtp1071550332-34', xff='', rId='70394abe-7ab6-4b7c-aaf5-17abfcdb9622', aRId='', tRId='', 1 * Server in-bound request\n" +
+ "1 > GET http://127.0.0.1:8080/1.0/kb/security/permissions\n" +
+ "1 > User-Agent: killbill/1.9.0; jruby 9.1.12.0 (2.3.3) 2017-06-15 33c6439 Java HotSpot(TM) 64-Bit Server VM 25.121-b13 on 1.8.0_121-b13 +jit [darwin-x86_64]\n" +
+ "1 > Authorization: **************************\n" +
+ "1 > Host: 127.0.0.1:8080\n" +
+ "1 > Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\n" +
+ "1 > Accept: application/json\n" +
+ "1 >");
+ }
+
+ @Test(groups = "fast")
+ public void testApiSecret() throws Exception {
+ verify("2017-08-25T15:28:34,331+0000 lvl='INFO', log='LoggingFilter', th='qtp288887829-1845', xff='', rId='59c40009-ea68-4d87-9580-fe95e9a82c23', aRId='', tRId='11', 3896 * Server in-bound request\n" +
+ "3896 > GET http://127.0.0.1:8080/1.0/kb/paymentMethods/069a4daa-e752-486c-8e40-c9c4f9a732c4?withPluginInfo=true\n" +
+ "3896 > Cookie: JSESSIONID=64faafa1-da74-4ac7-afc7-947cc9871fe5\n" +
+ "3896 > X-Killbill-Apikey: bob\n" +
+ "3896 > Accept: application/json\n" +
+ "3896 > X-Request-Id: 59c40009-ea68-4d87-9580-fe95e9a82c23\n" +
+ "3896 > X-Killbill-Apisecret: lazar\n" +
+ "3896 > User-Agent: killbill/1.9.0; ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin16]\n" +
+ "3896 > Host: 127.0.0.1:8080\n" +
+ "3896 > Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\n" +
+ "3896 >",
+ "2017-08-25T15:28:34,331+0000 lvl='INFO', log='LoggingFilter', th='qtp288887829-1845', xff='', rId='59c40009-ea68-4d87-9580-fe95e9a82c23', aRId='', tRId='11', 3896 * Server in-bound request\n" +
+ "3896 > GET http://127.0.0.1:8080/1.0/kb/paymentMethods/069a4daa-e752-486c-8e40-c9c4f9a732c4?withPluginInfo=true\n" +
+ "3896 > Cookie: JSESSIONID=64faafa1-da74-4ac7-afc7-947cc9871fe5\n" +
+ "3896 > X-Killbill-Apikey: bob\n" +
+ "3896 > Accept: application/json\n" +
+ "3896 > X-Request-Id: 59c40009-ea68-4d87-9580-fe95e9a82c23\n" +
+ "3896 > X-Killbill-Apisecret: *****\n" +
+ "3896 > User-Agent: killbill/1.9.0; ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin16]\n" +
+ "3896 > Host: 127.0.0.1:8080\n" +
+ "3896 > Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\n" +
+ "3896 >");
+ }
+
+ private void verify(final String input, final String output) {
+ final String obfuscated = obfuscator.obfuscate(input, Mockito.mock(ILoggingEvent.class));
+ Assert.assertEquals(obfuscated, output, obfuscated);
+ }
+}
diff --git a/profiles/killpay/src/main/java/org/killbill/billing/server/modules/KillpayServerModule.java b/profiles/killpay/src/main/java/org/killbill/billing/server/modules/KillpayServerModule.java
index 0f6f52d..b4b8f6d 100644
--- a/profiles/killpay/src/main/java/org/killbill/billing/server/modules/KillpayServerModule.java
+++ b/profiles/killpay/src/main/java/org/killbill/billing/server/modules/KillpayServerModule.java
@@ -52,7 +52,6 @@ import org.killbill.billing.server.config.KillbillServerConfig;
import org.killbill.billing.subscription.glue.DefaultSubscriptionModule;
import org.killbill.billing.tenant.glue.DefaultTenantModule;
import org.killbill.billing.usage.glue.UsageModule;
-import org.killbill.billing.util.email.EmailModule;
import org.killbill.billing.util.email.templates.TemplateModule;
import org.killbill.billing.util.glue.AuditModule;
import org.killbill.billing.util.glue.BroadcastModule;
@@ -112,7 +111,6 @@ public class KillpayServerModule extends KillbillServerModule {
install(new DefaultJaxrsModule(configSource));
// TODO Dependencies for AccountResource
install(new DefaultOverdueModule(configSource));
- install(new EmailModule(configSource));
}
@Override
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java b/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java
index 1ce9758..278ea3c 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java
@@ -63,6 +63,7 @@ public class PlanAligner extends BaseAligner {
NEXT
}
+
/**
* Returns the current and next phase for the subscription in creation
*
@@ -88,8 +89,8 @@ public class PlanAligner extends BaseAligner {
bundleStartDate,
plan,
initialPhase,
- effectiveDate,
catalog,
+ effectiveDate,
context);
final TimedPhase[] result = new TimedPhase[2];
result[0] = getTimedPhase(timedPhases, effectiveDate, WhichPhase.CURRENT);
@@ -164,8 +165,12 @@ public class PlanAligner extends BaseAligner {
subscription.getBundleStartDate(),
pendingOrLastPlanTransition.getNextPlan(),
pendingOrLastPlanTransition.getNextPhase().getPhaseType(),
- effectiveDate,
catalog,
+ // Use the catalog version at subscription creation time: this allows
+ // for PHASE events and uncancel operations for plans/products/pricelists already retired
+ // This is not 100% correct in a scenario where the catalog was updated and the alignment rules changed since
+ // See https://github.com/killbill/killbill/issues/784
+ subscription.getAlignStartDate(),
context);
return getTimedPhase(timedPhases, effectiveDate, WhichPhase.NEXT);
case CHANGE:
@@ -175,6 +180,8 @@ public class PlanAligner extends BaseAligner {
pendingOrLastPlanTransition.getPreviousPlan(),
pendingOrLastPlanTransition.getNextPlan(),
effectiveDate,
+ // Same remark as above
+ subscription.getAlignStartDate(),
pendingOrLastPlanTransition.getEffectiveTransitionTime(),
subscription.getAllTransitions().get(0).getNextPhase().getPhaseType(),
null,
@@ -194,14 +201,14 @@ public class PlanAligner extends BaseAligner {
final DateTime bundleStartDate,
final Plan plan,
@Nullable final PhaseType initialPhase,
- final DateTime effectiveDate,
final Catalog catalog,
+ final DateTime catalogEffectiveDate,
final InternalTenantContext context)
throws CatalogApiException, SubscriptionBaseApiException {
final PlanSpecifier planSpecifier = new PlanSpecifier(plan.getName());
final DateTime planStartDate;
- final PlanAlignmentCreate alignment = catalog.planCreateAlignment(planSpecifier, effectiveDate);
+ final PlanAlignmentCreate alignment = catalog.planCreateAlignment(planSpecifier, catalogEffectiveDate);
switch (alignment) {
case START_OF_SUBSCRIPTION:
planStartDate = subscriptionStartDate;
@@ -235,6 +242,7 @@ public class PlanAligner extends BaseAligner {
pendingOrLastPlanTransition.getNextPlan(),
nextPlan,
effectiveDate,
+ effectiveDate,
// This method is only called while doing the change, hence we want to pass the change effective date
effectiveDate,
subscription.getAllTransitions().get(0).getNextPhase().getPhaseType(),
@@ -250,6 +258,7 @@ public class PlanAligner extends BaseAligner {
final Plan currentPlan,
final Plan nextPlan,
final DateTime effectiveDate,
+ final DateTime catalogEffectiveDate,
final DateTime lastOrCurrentChangeEffectiveDate,
final PhaseType originalInitialPhase,
@Nullable final PhaseType newPlanInitialPhaseType,
@@ -262,7 +271,7 @@ public class PlanAligner extends BaseAligner {
final PlanSpecifier toPlanSpecifier = new PlanSpecifier(nextPlan.getName());
final PhaseType initialPhase;
final DateTime planStartDate;
- final PlanAlignmentChange alignment = catalog.planChangeAlignment(fromPlanPhaseSpecifier, toPlanSpecifier, effectiveDate);
+ final PlanAlignmentChange alignment = catalog.planChangeAlignment(fromPlanPhaseSpecifier, toPlanSpecifier, catalogEffectiveDate);
switch (alignment) {
case START_OF_SUBSCRIPTION:
planStartDate = subscriptionStartDate;
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
index 4d304a4..7bcba98 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
@@ -31,6 +31,7 @@ import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanChangeResult;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
import org.killbill.billing.subscription.api.user.SubscriptionAndAddOnsSpecifier;
@@ -66,19 +67,19 @@ public interface SubscriptionBaseApiService {
throws SubscriptionBaseApiException;
// Return the effective date of the change
- public DateTime dryRunChangePlan(DefaultSubscriptionBase subscription, PlanSpecifier spec, DateTime requestedDate, BillingActionPolicy policy, TenantContext context) throws SubscriptionBaseApiException;
+ public DateTime dryRunChangePlan(DefaultSubscriptionBase subscription, PlanPhaseSpecifier spec, DateTime requestedDate, BillingActionPolicy policy, TenantContext context) throws SubscriptionBaseApiException;
// Return the effective date of the change
- public DateTime changePlan(DefaultSubscriptionBase subscription, PlanSpecifier spec, List<PlanPhasePriceOverride> overrides, CallContext context)
+ public DateTime changePlan(DefaultSubscriptionBase subscription, PlanPhaseSpecifier spec, List<PlanPhasePriceOverride> overrides, CallContext context)
throws SubscriptionBaseApiException;
// Return the effective date of the change
- public DateTime changePlanWithRequestedDate(DefaultSubscriptionBase subscription, PlanSpecifier spec,
+ public DateTime changePlanWithRequestedDate(DefaultSubscriptionBase subscription, PlanPhaseSpecifier spec,
List<PlanPhasePriceOverride> overrides, DateTime requestedDate, CallContext context)
throws SubscriptionBaseApiException;
// Return the effective date of the change
- public DateTime changePlanWithPolicy(DefaultSubscriptionBase subscription, PlanSpecifier spec,
+ public DateTime changePlanWithPolicy(DefaultSubscriptionBase subscription, PlanPhaseSpecifier spec,
List<PlanPhasePriceOverride> overrides, BillingActionPolicy policy, CallContext context)
throws SubscriptionBaseApiException;
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 3a11873..a3a15cc 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
@@ -384,48 +384,16 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
@Override
public SubscriptionBaseBundle createBundleForAccount(final UUID accountId, final String bundleKey, final InternalCallContext context) throws SubscriptionBaseApiException {
+ final DateTime now = clock.getUTCNow();
+ final DefaultSubscriptionBaseBundle bundle = new DefaultSubscriptionBaseBundle(bundleKey, accountId, now, now, now, now);
+ if (null != bundleKey && bundleKey.length() > 255) {
+ throw new SubscriptionBaseApiException(ErrorCode.EXTERNAL_KEY_LIMIT_EXCEEDED);
+ }
try {
final Catalog catalog = catalogInternalApi.getFullCatalog(true, true, context);
-
- final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(bundleKey, context);
-
- //
- // Because the creation of the SubscriptionBundle is not atomic (with creation of Subscription/SubscriptionEvent), we verify if we were left
- // with an empty SubscriptionBaseBundle form a past failing operation (See #684). We only allow reuse if such SubscriptionBaseBundle is fully
- // empty (and don't allow use case where all Subscription are cancelled, which is the condition for that key to be re-used)
- // Such condition should have been checked upstream (to decide whether that key is valid or not)
- //
- final SubscriptionBaseBundle existingBundleForAccount = Iterables.tryFind(existingBundles, new Predicate<SubscriptionBaseBundle>() {
- @Override
- public boolean apply(final SubscriptionBaseBundle input) {
- return input.getAccountId().equals(accountId);
- }
- }).orNull();
-
- // If Bundle already exists, and there is 0 Subscription, we reuse
- if (existingBundleForAccount != null) {
- try {
- final Map<UUID, List<SubscriptionBase>> accountSubscriptions = dao.getSubscriptionsForAccount(catalog, context);
- final List<SubscriptionBase> subscriptions = accountSubscriptions.get(existingBundleForAccount.getId());
- if (subscriptions == null || subscriptions.size() == 0) {
- return existingBundleForAccount;
- }
- } catch (final CatalogApiException e) {
- throw new SubscriptionBaseApiException(e);
- }
- }
-
- final DateTime now = clock.getUTCNow();
- final DateTime originalCreatedDate = !existingBundles.isEmpty() ? existingBundles.get(0).getCreatedDate() : now;
- final DefaultSubscriptionBaseBundle bundle = new DefaultSubscriptionBaseBundle(bundleKey, accountId, now, originalCreatedDate, now, now);
-
- if (null != bundleKey && bundleKey.length() > 255) {
- throw new SubscriptionBaseApiException(ErrorCode.EXTERNAL_KEY_LIMIT_EXCEEDED);
- }
- return dao.createSubscriptionBundle(bundle, context);
-
- } catch (CatalogApiException e) {
- throw new SubscriptionBaseApiException(e);
+ return dao.createSubscriptionBundle(bundle, catalog, context);
+ } catch (final CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
}
}
@@ -624,7 +592,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
@Override
public DateTime getDryRunChangePlanEffectiveDate(final SubscriptionBase subscription,
- final PlanSpecifier spec,
+ final PlanPhaseSpecifier spec,
final DateTime requestedDateWithMs,
final BillingActionPolicy requestedPolicy,
final List<PlanPhasePriceOverride> overrides,
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 05c2133..1543d9b 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
@@ -40,6 +40,7 @@ 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.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.Product;
@@ -273,19 +274,19 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
}
@Override
- public DateTime changePlan(final PlanSpecifier spec,
+ public DateTime changePlan(final PlanPhaseSpecifier spec,
final List<PlanPhasePriceOverride> overrides, final CallContext context) throws SubscriptionBaseApiException {
return apiService.changePlan(this, spec, overrides, context);
}
@Override
- public DateTime changePlanWithDate(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides,
+ public DateTime changePlanWithDate(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides,
final DateTime requestedDate, final CallContext context) throws SubscriptionBaseApiException {
return apiService.changePlanWithRequestedDate(this, spec, overrides, requestedDate, context);
}
@Override
- public DateTime changePlanWithPolicy(final PlanSpecifier spec,
+ public DateTime changePlanWithPolicy(final PlanPhaseSpecifier spec,
final List<PlanPhasePriceOverride> overrides, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
return apiService.changePlanWithPolicy(this, spec, overrides, policy, context);
}
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 6fd4439..60d5928 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
@@ -323,7 +323,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
@Override
public DateTime dryRunChangePlan(final DefaultSubscriptionBase subscription,
- final PlanSpecifier spec,
+ final PlanPhaseSpecifier spec,
@Nullable final DateTime requestedDateWithMs,
@Nullable final BillingActionPolicy requestedPolicy,
final TenantContext context) throws SubscriptionBaseApiException {
@@ -345,7 +345,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
}
@Override
- public DateTime changePlan(final DefaultSubscriptionBase subscription, final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final CallContext context) throws SubscriptionBaseApiException {
+ public DateTime changePlan(final DefaultSubscriptionBase subscription, final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final CallContext context) throws SubscriptionBaseApiException {
final DateTime now = clock.getUTCNow();
validateSubscriptionState(subscription, null);
@@ -364,7 +364,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
}
@Override
- public DateTime changePlanWithRequestedDate(final DefaultSubscriptionBase subscription, final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides,
+ public DateTime changePlanWithRequestedDate(final DefaultSubscriptionBase subscription, final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides,
final DateTime requestedDateWithMs, final CallContext context) throws SubscriptionBaseApiException {
final DateTime effectiveDate = dryRunChangePlan(subscription, spec, requestedDateWithMs, null, context);
validateEffectiveDate(subscription, effectiveDate);
@@ -380,7 +380,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
}
@Override
- public DateTime changePlanWithPolicy(final DefaultSubscriptionBase subscription, final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
+ public DateTime changePlanWithPolicy(final DefaultSubscriptionBase subscription, final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
final DateTime effectiveDate = dryRunChangePlan(subscription, spec, null, policy, context);
@@ -412,7 +412,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
}
private void doChangePlan(final DefaultSubscriptionBase subscription,
- final PlanSpecifier spec,
+ final PlanPhaseSpecifier spec,
final List<PlanPhasePriceOverride> overrides,
final DateTime effectiveDate,
final CallContext context) throws SubscriptionBaseApiException, CatalogApiException {
@@ -423,16 +423,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
final Catalog fullCatalog = catalogInternalApi.getFullCatalog(true, true, internalCallContext);
final Plan newPlan = fullCatalog.createOrFindPlan(spec, overridesWithContext, effectiveDate, subscription.getStartDate());
- final PhaseType initialPhaseType;
- if (overrides != null &&
- overrides.size() == 1 &&
- overrides.get(0).getPlanPhaseSpecifier() != null &&
- overrides.get(0).getCurrency() == null) {
- initialPhaseType = overrides.get(0).getPlanPhaseSpecifier().getPhaseType();
- } else {
- initialPhaseType = null;
- }
-
+ final PhaseType initialPhaseType = spec.getPhaseType();
if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(newPlan.getProduct().getCategory().toString())) {
if (newPlan.getPlansAllowedInBundle() != -1
&& newPlan.getPlansAllowedInBundle() > 0
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 ed363e6..ac018a3 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
@@ -43,6 +43,7 @@ import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogInternalApi;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.api.SubscriptionApiException;
import org.killbill.billing.entity.EntityPersistenceException;
import org.killbill.billing.subscription.api.SubscriptionBase;
@@ -56,6 +57,7 @@ import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEv
import org.killbill.billing.subscription.api.user.DefaultRequestedSubscriptionEvent;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
@@ -255,11 +257,77 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
}
@Override
- public SubscriptionBaseBundle createSubscriptionBundle(final DefaultSubscriptionBaseBundle bundle, final InternalCallContext context) {
- return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<SubscriptionBaseBundle>() {
+ public SubscriptionBaseBundle createSubscriptionBundle(final DefaultSubscriptionBaseBundle bundle, final Catalog catalog, final InternalCallContext context) throws SubscriptionBaseApiException {
+
+
+ return transactionalSqlDao.execute(SubscriptionBaseApiException.class, new EntitySqlDaoTransactionWrapper<SubscriptionBaseBundle>() {
+
+ //
+ // Because the creation of the SubscriptionBundle is not atomic (with creation of Subscription/SubscriptionEvent), we verify if we were left
+ // with an empty SubscriptionBaseBundle form a past failing operation (See #684). We only allow reuse if such SubscriptionBaseBundle is fully
+ // empty (and don't allow use case where all Subscription are cancelled, which is the condition for that key to be re-used)
+ // Such condition should have been checked upstream (to decide whether that key is valid or not)
+ //
+
+ private SubscriptionBaseBundle findExistingUnusedBundleForExternalKeyAndAccount(final List<SubscriptionBundleModelDao> existingBundles, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) {
+ final SubscriptionBundleModelDao existingBundleForAccount = Iterables.tryFind(existingBundles, new Predicate<SubscriptionBundleModelDao>() {
+ @Override
+ public boolean apply(final SubscriptionBundleModelDao input) {
+ return input.getAccountId().equals(bundle.getAccountId());
+ }
+ }).orNull();
+
+ // If Bundle already exists, and there is 0 Subscription associated with this bundle, we reuse
+ if (existingBundleForAccount != null) {
+ final List<SubscriptionModelDao> accountSubscriptions = entitySqlDaoWrapperFactory.become(SubscriptionSqlDao.class).getByAccountRecordId(context);
+ if (accountSubscriptions == null ||
+ ! Iterables.any(accountSubscriptions, new Predicate<SubscriptionModelDao>() {
+ @Override
+ public boolean apply(final SubscriptionModelDao input) {
+ return input.getBundleId().equals(existingBundleForAccount.getId());
+ }
+ })) {
+ return SubscriptionBundleModelDao.toSubscriptionbundle(existingBundleForAccount);
+ }
+ }
+ return null;
+ }
+
+
@Override
- public SubscriptionBaseBundle inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws EntityPersistenceException {
+ public SubscriptionBaseBundle inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+ final List<SubscriptionBundleModelDao> existingBundles = entitySqlDaoWrapperFactory.become(BundleSqlDao.class).getBundlesForKey(bundle.getExternalKey(), context);
+
+ final SubscriptionBaseBundle unusedBundle = findExistingUnusedBundleForExternalKeyAndAccount(existingBundles, entitySqlDaoWrapperFactory);
+ if (unusedBundle != null) {
+ return unusedBundle;
+ }
+
+ for (SubscriptionBundleModelDao cur : existingBundles) {
+ final List<SubscriptionModelDao> subscriptions = entitySqlDaoWrapperFactory.become(SubscriptionSqlDao.class).getSubscriptionsFromBundleId(cur.getId().toString(), context);
+ final Iterable<SubscriptionModelDao> filtered = subscriptions != null ? Iterables.filter(subscriptions, new Predicate<SubscriptionModelDao>() {
+ @Override
+ public boolean apply(final SubscriptionModelDao input) {
+ return input.getCategory() != ProductCategory.ADD_ON;
+ }
+ }) : ImmutableList.<SubscriptionModelDao>of();
+ for (SubscriptionModelDao f : filtered) {
+ try {
+ final SubscriptionBase s = buildSubscription(SubscriptionModelDao.toSubscription(f, cur.getExternalKey()), catalog, context);
+ if (s.getState() != EntitlementState.CANCELLED) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS, bundle.getExternalKey());
+ }
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
+ }
+ }
+ }
+
final SubscriptionBundleModelDao model = new SubscriptionBundleModelDao(bundle);
+ // Preserve Original created date
+ if (!existingBundles.isEmpty()) {
+ model.setOriginalCreatedDate(existingBundles.get(0).getCreatedDate());
+ }
final BundleSqlDao bundleSqlDao = entitySqlDaoWrapperFactory.become(BundleSqlDao.class);
final SubscriptionBundleModelDao result = createAndRefresh(bundleSqlDao, model, context);
return SubscriptionBundleModelDao.toSubscriptionbundle(result);
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 be85bb2..ac499bf 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
@@ -32,6 +32,7 @@ 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.DefaultSubscriptionBaseWithAddOns;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
@@ -53,7 +54,7 @@ public interface SubscriptionDao extends EntityDao<SubscriptionBundleModelDao, S
public SubscriptionBaseBundle getSubscriptionBundleFromId(UUID bundleId, InternalTenantContext context);
- public SubscriptionBaseBundle createSubscriptionBundle(DefaultSubscriptionBaseBundle bundle, InternalCallContext context);
+ public SubscriptionBaseBundle createSubscriptionBundle(DefaultSubscriptionBaseBundle bundle, final Catalog catalog, InternalCallContext context) throws SubscriptionBaseApiException;
public SubscriptionBase getSubscriptionFromId(UUID subscriptionId, final Catalog catalog, InternalTenantContext context) throws CatalogApiException;
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java
index bbbd1e4..3a0b350 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java
@@ -28,7 +28,7 @@ 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.PlanSpecifier;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
@@ -70,7 +70,6 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
newAccountId = account.getId();
}
-
@Test(groups = "slow")
public void testTransferBPInTrialWithNoCTD() throws Exception {
final String baseProduct = "Shotgun";
@@ -267,7 +266,7 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
final String newBaseProduct1 = "Assault-Rifle";
final BillingPeriod newBaseTerm1 = BillingPeriod.ANNUAL;
testListener.pushExpectedEvent(NextEvent.CHANGE);
- newBaseSubscription.changePlan(new PlanSpecifier(newBaseProduct1, newBaseTerm1, basePriceList), null, callContext);
+ newBaseSubscription.changePlan(new PlanPhaseSpecifier(newBaseProduct1, newBaseTerm1, basePriceList), null, callContext);
assertListenerStatus();
newPlan = newBaseSubscription.getCurrentPlan();
@@ -283,7 +282,7 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
final String newBaseProduct2 = "Pistol";
final BillingPeriod newBaseTerm2 = BillingPeriod.ANNUAL;
- newBaseSubscriptionWithCtd.changePlan(new PlanSpecifier(newBaseProduct2, newBaseTerm2, basePriceList), null, callContext);
+ newBaseSubscriptionWithCtd.changePlan(new PlanPhaseSpecifier(newBaseProduct2, newBaseTerm2, basePriceList), null, callContext);
newPlan = newBaseSubscriptionWithCtd.getCurrentPlan();
assertEquals(newPlan.getProduct().getName(), newBaseProduct1);
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java
index cb9e43a..28b6a80 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java
@@ -20,8 +20,6 @@ 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.CatalogApiException;
@@ -30,6 +28,7 @@ import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanAlignmentCreate;
import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
@@ -38,6 +37,7 @@ import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun.DryRunChangeReason;
import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
@@ -311,7 +311,7 @@ public class TestUserApiAddOn extends SubscriptionTestSuiteWithEmbeddedDB {
testListener.pushExpectedEvent(NextEvent.CHANGE);
testListener.pushExpectedEvent(NextEvent.CANCEL);
- baseSubscription.changePlan(new PlanSpecifier(newBaseProduct, newBaseTerm, newBasePriceList), null, callContext);
+ baseSubscription.changePlan(new PlanPhaseSpecifier(newBaseProduct, newBaseTerm, newBasePriceList, null), null, callContext);
assertListenerStatus();
// REFETCH AO SUBSCRIPTION AND CHECK THIS CANCELLED
@@ -367,7 +367,7 @@ public class TestUserApiAddOn extends SubscriptionTestSuiteWithEmbeddedDB {
assertEquals(aoStatus.get(0).getPriceList(), aoSubscription.getCurrentPriceList().getName());
assertEquals(aoStatus.get(0).getReason(), DryRunChangeReason.AO_NOT_AVAILABLE_IN_NEW_PLAN);
- baseSubscription.changePlan(new PlanSpecifier(newBaseProduct, newBaseTerm, newBasePriceList), null, callContext);
+ baseSubscription.changePlan(new PlanPhaseSpecifier(newBaseProduct, newBaseTerm, newBasePriceList), null, callContext);
// REFETCH AO SUBSCRIPTION AND CHECK THIS IS ACTIVE
aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
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 57d5026..d7d20b5 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
@@ -35,7 +35,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.catalog.api.PlanPhaseSpecifier;
-import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.Entitlement;
@@ -105,7 +104,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
// CHANGE PLAN
testListener.pushExpectedEvent(NextEvent.CHANGE);
- subscription.changePlan(new PlanSpecifier(toProd, toTerm, toPlanSet), null, callContext);
+ subscription.changePlan(new PlanPhaseSpecifier(toProd, toTerm, toPlanSet), null, callContext);
assertListenerStatus();
// CHECK CHANGE PLAN
@@ -146,7 +145,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
// RE READ SUBSCRIPTION + CHANGE PLAN
subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
- subscription.changePlan(new PlanSpecifier(toProd, toTerm, toPlanSet), null, callContext);
+ subscription.changePlan(new PlanPhaseSpecifier(toProd, toTerm, toPlanSet), null, callContext);
assertListenerStatus();
// CHECK CHANGE PLAN
@@ -189,7 +188,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
clock.addDeltaFromReality(it.toDurationMillis());
// CHANGE PLAN IMM
- subscription.changePlan(new PlanSpecifier(toProd, toTerm, toPlanSet), null, callContext);
+ subscription.changePlan(new PlanPhaseSpecifier(toProd, toTerm, toPlanSet), null, callContext);
checkChangePlan(subscription, toProd, ProductCategory.BASE, toTerm, PhaseType.TRIAL);
assertListenerStatus();
@@ -243,7 +242,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
// CHANGE PLAN
currentTime = clock.getUTCNow();
- subscription.changePlan(new PlanSpecifier(toProd, toTerm, toPlanSet), null, callContext);
+ subscription.changePlan(new PlanPhaseSpecifier(toProd, toTerm, toPlanSet), null, callContext);
checkChangePlan(subscription, fromProd, ProductCategory.BASE, fromTerm, PhaseType.EVERGREEN);
@@ -309,12 +308,12 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
// CHANGE EOT
- subscription.changePlan(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, "gunclubDiscount"), null, callContext);
+ subscription.changePlan(new PlanPhaseSpecifier("Pistol", BillingPeriod.MONTHLY, "gunclubDiscount"), null, callContext);
assertListenerStatus();
// CHANGE
testListener.pushExpectedEvent(NextEvent.CHANGE);
- subscription.changePlan(new PlanSpecifier("Assault-Rifle", BillingPeriod.ANNUAL, "gunclubDiscount"), null, callContext);
+ subscription.changePlan(new PlanPhaseSpecifier("Assault-Rifle", BillingPeriod.ANNUAL, "gunclubDiscount"), null, callContext);
assertListenerStatus();
final Plan currentPlan = subscription.getCurrentPlan();
@@ -351,11 +350,11 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
// CHANGE EOT
- subscription.changePlan(new PlanSpecifier("Shotgun", BillingPeriod.MONTHLY, "gunclubDiscount"), null, callContext);
+ subscription.changePlan(new PlanPhaseSpecifier("Shotgun", BillingPeriod.MONTHLY, "gunclubDiscount"), null, callContext);
assertListenerStatus();
// CHANGE EOT
- subscription.changePlan(new PlanSpecifier("Pistol", BillingPeriod.ANNUAL, "gunclubDiscount"), null, callContext);
+ subscription.changePlan(new PlanPhaseSpecifier("Pistol", BillingPeriod.ANNUAL, "gunclubDiscount"), null, callContext);
assertListenerStatus();
// CHECK NO CHANGE OCCURED YET
@@ -417,7 +416,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
// CHANGE IMMEDIATE TO A 3 PHASES PLAN
testListener.pushExpectedEvent(NextEvent.CHANGE);
- subscription.changePlan(new PlanSpecifier("Assault-Rifle", BillingPeriod.ANNUAL, "gunclubDiscount"), null, callContext);
+ subscription.changePlan(new PlanPhaseSpecifier("Assault-Rifle", BillingPeriod.ANNUAL, "gunclubDiscount"), null, callContext);
assertListenerStatus();
// CHECK EVERYTHING LOOKS CORRECT
@@ -471,7 +470,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, aoProduct, aoTerm, aoPriceList);
try {
- aoSubscription.changePlanWithDate(new PlanSpecifier(baseProduct, baseTerm, basePriceList), null, clock.getUTCNow(), callContext);
+ aoSubscription.changePlanWithDate(new PlanPhaseSpecifier(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());
@@ -493,9 +492,8 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Pistol", baseTerm, basePriceList, null);
-
// First try with default api (no date -> IMM) => Call should fail because subscription is PENDING
- final DryRunArguments dryRunArguments1 = testUtil.createDryRunArguments(subscription.getId(), subscription.getBundleId(), spec, null, SubscriptionEventType.CHANGE, null);
+ final DryRunArguments dryRunArguments1 = testUtil.createDryRunArguments(subscription.getId(), subscription.getBundleId(), spec, null, SubscriptionEventType.CHANGE, null);
final List<SubscriptionBase> result1 = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), dryRunArguments1, internalCallContext);
// Check we are seeing the right PENDING transition (pistol-monthly), not the START but the CHANGE on the same date
@@ -504,7 +502,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
// Second try with date prior to startDate => Call should fail because subscription is PENDING
try {
- final DryRunArguments dryRunArguments2 = testUtil.createDryRunArguments(subscription.getId(), subscription.getBundleId(), spec, new LocalDate(startDate.minusDays(1)), SubscriptionEventType.CHANGE, null);
+ final DryRunArguments dryRunArguments2 = testUtil.createDryRunArguments(subscription.getId(), subscription.getBundleId(), spec, new LocalDate(startDate.minusDays(1)), SubscriptionEventType.CHANGE, null);
subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), dryRunArguments2, internalCallContext);
fail("Change plan should have failed : subscription PENDING");
} catch (final SubscriptionBaseApiException e) {
@@ -518,12 +516,11 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
}
// Third try with date equals to startDate Call should succeed, but no event because action in future
- final DryRunArguments dryRunArguments3 = testUtil.createDryRunArguments(subscription.getId(), subscription.getBundleId(), spec, internalCallContext.toLocalDate(startDate), SubscriptionEventType.CHANGE, null);
+ final DryRunArguments dryRunArguments3 = testUtil.createDryRunArguments(subscription.getId(), subscription.getBundleId(), spec, internalCallContext.toLocalDate(startDate), SubscriptionEventType.CHANGE, null);
final List<SubscriptionBase> result2 = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), dryRunArguments3, internalCallContext);
// Check we are seeing the right PENDING transition (pistol-monthly), not the START but the CHANGE on the same date
assertEquals(((DefaultSubscriptionBase) result2.get(0)).getCurrentOrPendingPlan().getName(), "pistol-monthly");
-
// To spice up the test, we insert manually an additional CHANGE event on the exact same dateas the CREATE to verify that code will also invalidate such event when doing the changePlanXX
final SubscriptionEventModelDao event = new SubscriptionEventModelDao(subscription.getEvents().get(0));
@@ -540,7 +537,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
}
});
- final DefaultSubscriptionBase refreshed1 = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+ final DefaultSubscriptionBase refreshed1 = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
assertEquals(refreshed1.getEvents().size(), subscription.getEvents().size() + 1);
subscription.changePlanWithDate(spec, null, startDate, callContext);
@@ -565,7 +562,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
// CHANGE PLAN IMMEDIATELY: the CHANGE event will be transformed into a CREATE
testListener.pushExpectedEvent(NextEvent.CREATE);
- subscription.changePlanWithDate(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, subscription.getStartDate(), callContext);
+ subscription.changePlanWithDate(new PlanPhaseSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, subscription.getStartDate(), callContext);
assertListenerStatus();
checkChangePlan(subscription, "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PhaseType.TRIAL);
@@ -584,7 +581,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
// CHANGE PLAN ALMOST IMMEDIATELY
testListener.pushExpectedEvent(NextEvent.CHANGE);
- subscription.changePlan(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, callContext);
+ subscription.changePlan(new PlanPhaseSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, callContext);
assertListenerStatus();
checkChangePlan(subscription, "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PhaseType.TRIAL);
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java
index 4776379..36167c8 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java
@@ -20,6 +20,7 @@ import java.util.List;
import org.joda.time.DateTime;
import org.joda.time.Interval;
+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;
@@ -62,6 +63,15 @@ public class TestUserApiCreate extends SubscriptionTestSuiteWithEmbeddedDB {
assertListenerStatus();
assertNotNull(subscription);
+
+ // Verify we can't create a second bundle with the same key
+ try {
+ subscriptionInternalApi.createBundleForAccount(bundle.getAccountId(), DefaultSubscriptionTestInitializer.DEFAULT_BUNDLE_KEY, internalCallContext);
+ Assert.fail("Should not be able to create a bundle with same externalKey");
+ } catch (final SubscriptionBaseApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS.getCode());
+ }
+
testListener.pushExpectedEvent(NextEvent.CANCEL);
subscription.cancelWithDate(clock.getUTCNow(), callContext);
assertListenerStatus();
@@ -70,6 +80,7 @@ public class TestUserApiCreate extends SubscriptionTestSuiteWithEmbeddedDB {
assertNotNull(newBundle);
assertEquals(newBundle.getOriginalCreatedDate().compareTo(bundle.getCreatedDate()), 0);
+
testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.PHASE);
final DefaultSubscriptionBase newSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.createSubscription(newBundle.getId(),
testUtil.getProductSpecifier(productName, planSetName, term, null), null, requestedDate, false, internalCallContext);
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
index 6891b28..f852cae 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
@@ -30,7 +30,7 @@ import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Duration;
import org.killbill.billing.catalog.api.PlanPhase;
-import org.killbill.billing.catalog.api.PlanSpecifier;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.subscription.SubscriptionTestSuiteNoDB;
import org.killbill.billing.subscription.api.SubscriptionBase;
@@ -113,7 +113,7 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
testListener.pushExpectedEvent(NextEvent.CANCEL);
subscription.cancelWithDate(clock.getUTCNow(), callContext);
try {
- subscription.changePlanWithDate(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, clock.getUTCNow(), callContext);
+ subscription.changePlanWithDate(new PlanPhaseSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, clock.getUTCNow(), callContext);
} catch (final SubscriptionBaseApiException e) {
assertEquals(e.getCode(), ErrorCode.SUB_CHANGE_NON_ACTIVE.getCode());
}
@@ -124,7 +124,7 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
final SubscriptionBase subscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME);
try {
- subscription.changePlanWithPolicy(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, BillingActionPolicy.ILLEGAL, callContext);
+ subscription.changePlanWithPolicy(new PlanPhaseSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, BillingActionPolicy.ILLEGAL, callContext);
Assert.fail();
} catch (final SubscriptionBaseError error) {
assertTrue(true);
@@ -132,7 +132,7 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
}
// Assume the call takes less than a second
- assertEquals(DefaultClock.truncateMs(subscription.changePlanWithPolicy(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, BillingActionPolicy.IMMEDIATE, callContext)),
+ assertEquals(DefaultClock.truncateMs(subscription.changePlanWithPolicy(new PlanPhaseSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, BillingActionPolicy.IMMEDIATE, callContext)),
DefaultClock.truncateMs(clock.getUTCNow()));
assertEquals(subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext).getCurrentPlan().getRecurringBillingPeriod(), BillingPeriod.MONTHLY);
}
@@ -159,7 +159,7 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
subscription.cancelWithPolicy(BillingActionPolicy.END_OF_TERM, -1, callContext);
try {
- subscription.changePlanWithDate(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, clock.getUTCNow(), callContext);
+ subscription.changePlanWithDate(new PlanPhaseSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, clock.getUTCNow(), callContext);
} catch (final SubscriptionBaseApiException e) {
assertEquals(e.getCode(), ErrorCode.SUB_CHANGE_FUTURE_CANCELLED.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 1c2af52..cbcb9a5 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
@@ -174,7 +174,7 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
}
@Override
- public SubscriptionBaseBundle createSubscriptionBundle(final DefaultSubscriptionBaseBundle bundle, final InternalCallContext context) {
+ public SubscriptionBaseBundle createSubscriptionBundle(final DefaultSubscriptionBaseBundle bundle, final Catalog catalog, final InternalCallContext context) {
bundles.add(bundle);
mockNonEntityDao.addTenantRecordIdMapping(bundle.getId(), context);
return getSubscriptionBundleFromId(bundle.getId(), context);
util/pom.xml 8(+4 -4)
diff --git a/util/pom.xml b/util/pom.xml
index a043aae..8222bf9 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -75,6 +75,10 @@
</dependency>
<dependency>
<groupId>com.ning</groupId>
+ <artifactId>async-http-client</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.ning</groupId>
<artifactId>compress-lzf</artifactId>
<scope>test</scope>
</dependency>
@@ -138,10 +142,6 @@
<artifactId>ST4</artifactId>
</dependency>
<dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-email</artifactId>
- </dependency>
- <dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</dependency>
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 c53b879..7f059fc 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
@@ -115,7 +115,7 @@ public class EhCacheBasedCacheController<K, V> implements CacheController<K, V>
@Override
public int size() {
- return Iterables.<Cache.Entry<K, V>>size(cache);
+ return Iterables.size(cache);
}
@Override
diff --git a/util/src/main/java/org/killbill/billing/util/config/definition/SecurityConfig.java b/util/src/main/java/org/killbill/billing/util/config/definition/SecurityConfig.java
index cfd6a84..f0e5c99 100644
--- a/util/src/main/java/org/killbill/billing/util/config/definition/SecurityConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/definition/SecurityConfig.java
@@ -41,6 +41,11 @@ public interface SecurityConfig extends KillbillConfig {
@Description("LDAP server's User DN format (e.g. uid={0},ou=users,dc=mycompany,dc=com)")
public String getShiroLDAPUserDnTemplate();
+ @Config("org.killbill.security.ldap.dnSearchTemplate")
+ @DefaultNull
+ @Description("LDAP server's DN search template (e.g. sAMAccountName={0}) for search-then-bind authentication (in case a static DN format template isn't enough)")
+ public String getShiroLDAPDnSearchTemplate();
+
@Config("org.killbill.security.ldap.searchBase")
@DefaultNull
@Description("LDAP search base to use")
@@ -87,4 +92,28 @@ public interface SecurityConfig extends KillbillConfig {
@Default("false")
@Description("Whether to ignore SSL certificates checks")
public boolean disableShiroLDAPSSLCheck();
+
+ @Config("org.killbill.security.ldap.followReferrals")
+ @Default("false")
+ @Description("Whether to follow referrals")
+ public boolean followShiroLDAPReferrals();
+
+ // Okta realm
+
+ @Config("org.killbill.security.okta.url")
+ @DefaultNull
+ @Description("Okta org full url")
+ public String getShiroOktaUrl();
+
+ @Config("org.killbill.security.okta.apiToken")
+ @DefaultNull
+ @Description("Okta API token")
+ public String getShiroOktaAPIToken();
+
+ @Config("org.killbill.security.okta.permissionsByGroup")
+ @Default("admin = *:*\n" +
+ "finance = invoice:*, payment:*\n" +
+ "support = entitlement:*, invoice:item_adjust")
+ @Description("Okta permissions by Okta group")
+ public String getShiroOktaPermissionsByGroup();
}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
index bd9ceb2..c103789 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
@@ -28,6 +28,7 @@ import org.killbill.billing.util.config.definition.RbacConfig;
import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
import org.killbill.billing.util.security.shiro.realm.KillBillJdbcRealm;
import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
+import org.killbill.billing.util.security.shiro.realm.KillBillOktaRealm;
import org.skife.config.ConfigSource;
import org.skife.config.ConfigurationObjectFactory;
@@ -38,6 +39,7 @@ import com.google.inject.binder.AnnotatedBindingBuilder;
public class KillBillShiroModule extends ShiroModule {
public static final String KILLBILL_LDAP_PROPERTY = "killbill.server.ldap";
+ public static final String KILLBILL_OKTA_PROPERTY = "killbill.server.okta";
public static final String KILLBILL_RBAC_PROPERTY = "killbill.server.rbac";
@@ -45,6 +47,10 @@ public class KillBillShiroModule extends ShiroModule {
return Boolean.parseBoolean(System.getProperty(KILLBILL_LDAP_PROPERTY, "false"));
}
+ public static boolean isOktaEnabled() {
+ return Boolean.parseBoolean(System.getProperty(KILLBILL_OKTA_PROPERTY, "false"));
+ }
+
public static boolean isRBACEnabled() {
return Boolean.parseBoolean(System.getProperty(KILLBILL_RBAC_PROPERTY, "true"));
}
@@ -81,6 +87,12 @@ public class KillBillShiroModule extends ShiroModule {
}
}
+ protected void configureOktaRealm() {
+ if (isOktaEnabled()) {
+ bindRealm().to(KillBillOktaRealm.class).asEagerSingleton();
+ }
+ }
+
@Override
protected void bindSecurityManager(final AnnotatedBindingBuilder<? super SecurityManager> bind) {
super.bindSecurityManager(bind);
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionDao.java b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionDao.java
index a6a5a34..e241a20 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionDao.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionDao.java
@@ -41,7 +41,7 @@ public class JDBCSessionDao extends CachingSessionDAO {
private final JDBCSessionSqlDao jdbcSessionSqlDao;
- private final Cache<Serializable, Boolean> noUpdateSessionsCache = CacheBuilder.<Serializable, Boolean>newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).build();
+ private final Cache<Serializable, Boolean> noUpdateSessionsCache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).build();
@Inject
public JDBCSessionDao(final IDBI dbi) {
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java
index 7779670..ac15666 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java
@@ -39,16 +39,17 @@ import org.apache.shiro.realm.ldap.JndiLdapRealm;
import org.apache.shiro.realm.ldap.LdapContextFactory;
import org.apache.shiro.realm.ldap.LdapUtils;
import org.apache.shiro.subject.PrincipalCollection;
+import org.killbill.billing.util.config.definition.SecurityConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.killbill.billing.util.config.definition.SecurityConfig;
-
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
+import com.google.common.base.Functions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
+import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterators;
@@ -74,6 +75,7 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
private final String groupSearchFilter;
private final String groupNameId;
private final Map<String, Collection<String>> permissionsByGroup = Maps.newLinkedHashMap();
+ private final String dnSearchFilter;
@Inject
public KillBillJndiLdapRealm(final SecurityConfig securityConfig) {
@@ -87,6 +89,7 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
if (securityConfig.disableShiroLDAPSSLCheck()) {
contextFactory.getEnvironment().put("java.naming.ldap.factory.socket", SkipSSLCheckSocketFactory.class.getName());
}
+ contextFactory.getEnvironment().put("java.naming.referral", securityConfig.followShiroLDAPReferrals() ? "follow" : "ignore");
if (securityConfig.getShiroLDAPUrl() != null) {
contextFactory.setUrl(securityConfig.getShiroLDAPUrl());
}
@@ -101,6 +104,8 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
}
setContextFactory(contextFactory);
+ dnSearchFilter = securityConfig.getShiroLDAPDnSearchTemplate();
+
searchBase = securityConfig.getShiroLDAPSearchBase();
groupSearchFilter = securityConfig.getShiroLDAPGroupSearchFilter();
groupNameId = securityConfig.getShiroLDAPGroupNameID();
@@ -110,8 +115,10 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
// When passing properties on the command line, \n can be escaped
ini.load(securityConfig.getShiroLDAPPermissionsByGroup().replace("\\n", "\n"));
for (final Section section : ini.getSections()) {
- for (final String role : section.keySet()) {
- final Collection<String> permissions = ImmutableList.<String>copyOf(SPLITTER.split(section.get(role)));
+ for (final String rawRole : section.keySet()) {
+ // Un-escape manually = (required if the role name is a DN)
+ final Collection<String> permissions = ImmutableList.<String>copyOf(SPLITTER.split(section.get(rawRole)));
+ final String role = rawRole.replace("\\=", "=");
permissionsByGroup.put(role, permissions);
}
}
@@ -119,6 +126,35 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
}
@Override
+ protected String getUserDn(final String principal) throws IllegalArgumentException, IllegalStateException {
+ if (dnSearchFilter != null) {
+ return findUserDN(principal, getContextFactory());
+ } else {
+ // Use template
+ return super.getUserDn(principal);
+ }
+ }
+
+ private String findUserDN(final String userName, final LdapContextFactory ldapContextFactory) {
+ LdapContext systemLdapCtx = null;
+ try {
+ systemLdapCtx = ldapContextFactory.getSystemLdapContext();
+ final NamingEnumeration<SearchResult> usersFound = systemLdapCtx.search(searchBase,
+ dnSearchFilter.replace(USERDN_SUBSTITUTION_TOKEN, userName),
+ SUBTREE_SCOPE);
+ return usersFound.hasMore() ? usersFound.next().getNameInNamespace() : null;
+ } catch (final AuthenticationException ex) {
+ log.info("LDAP authentication exception='{}'", ex.getLocalizedMessage());
+ throw new IllegalArgumentException(ex);
+ } catch (final NamingException e) {
+ log.info("LDAP exception='{}'", e.getLocalizedMessage());
+ throw new IllegalArgumentException(e);
+ } finally {
+ LdapUtils.closeContext(systemLdapCtx);
+ }
+ }
+
+ @Override
protected AuthorizationInfo queryForAuthorizationInfo(final PrincipalCollection principals, final LdapContextFactory ldapContextFactory) throws NamingException {
final Set<String> userGroups = findLDAPGroupsForUser(principals, ldapContextFactory);
@@ -136,7 +172,7 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
try {
systemLdapCtx = ldapContextFactory.getSystemLdapContext();
return findLDAPGroupsForUser(username, systemLdapCtx);
- } catch (AuthenticationException ex) {
+ } catch (final AuthenticationException ex) {
log.info("LDAP authentication exception='{}'", ex.getLocalizedMessage());
return ImmutableSet.<String>of();
} finally {
@@ -149,21 +185,20 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
groupSearchFilter.replace(USERDN_SUBSTITUTION_TOKEN, userName),
SUBTREE_SCOPE);
+ if (!foundGroups.hasMoreElements()) {
+ return ImmutableSet.<String>of();
+ }
+
+ // There should really only be one entry
+ final SearchResult result = foundGroups.next();
+
// Extract the name of all the groups
- final Iterator<SearchResult> groupsIterator = Iterators.<SearchResult>forEnumeration(foundGroups);
- final Iterator<String> groupsNameIterator = Iterators.<SearchResult, String>transform(groupsIterator,
- new Function<SearchResult, String>() {
- @Override
- public String apply(final SearchResult groupEntry) {
- return extractGroupNameFromSearchResult(groupEntry);
- }
- });
- final Iterator<String> finalGroupsNameIterator = Iterators.<String>filter(groupsNameIterator, Predicates.notNull());
-
- return Sets.newHashSet(finalGroupsNameIterator);
+ final Collection<String> finalGroupsNames = Collections2.<String>filter(extractGroupNamesFromSearchResult(result), Predicates.notNull());
+
+ return Sets.newHashSet(finalGroupsNames);
}
- private String extractGroupNameFromSearchResult(final SearchResult searchResult) {
+ private Collection<String> extractGroupNamesFromSearchResult(final SearchResult searchResult) {
// Get all attributes for that group
final Iterator<? extends Attribute> attributesIterator = Iterators.forEnumeration(searchResult.getAttributes().getAll());
@@ -178,31 +213,23 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
// Extract the group name from the attribute
// Note: at this point, groupNameAttributesIterator should really contain a single element
- final Iterator<String> groupNamesIterator = Iterators.transform(groupNameAttributesIterator,
- new Function<Attribute, String>() {
+ final Iterator<Iterator<?>> groupNamesIterator = Iterators.transform(groupNameAttributesIterator,
+ new Function<Attribute, Iterator<?>>() {
@Override
- public String apply(final Attribute groupNameAttribute) {
+ public Iterator<?> apply(final Attribute groupNameAttribute) {
try {
final NamingEnumeration<?> enumeration = groupNameAttribute.getAll();
- if (enumeration.hasMore()) {
- return enumeration.next().toString();
- } else {
- return null;
- }
- } catch (NamingException namingException) {
- log.warn("Unable to read group name", namingException);
+ return Iterators.forEnumeration(enumeration);
+ } catch (final NamingException namingException) {
+ log.warn("Unable to read group name(s)", namingException);
return null;
}
}
});
- final Iterator<String> finalGroupNamesIterator = Iterators.<String>filter(groupNamesIterator, Predicates.notNull());
- if (finalGroupNamesIterator.hasNext()) {
- return finalGroupNamesIterator.next();
- } else {
- log.warn("Unable to find an attribute matching {}", groupNameId);
- return null;
- }
+ final Iterator<?> finalGroupNamesIterator = Iterators.filter(Iterators.concat(groupNamesIterator), Predicates.notNull());
+
+ return ImmutableList.<String>copyOf(Iterators.transform(finalGroupNamesIterator, Functions.toStringFunction()));
}
private Set<String> groupsPermissions(final Set<String> groups) {
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillOktaRealm.java b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillOktaRealm.java
new file mode 100644
index 0000000..8d75bab
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillOktaRealm.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * 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.security.shiro.realm;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.SimpleAuthenticationInfo;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.authz.AuthorizationInfo;
+import org.apache.shiro.authz.SimpleAuthorizationInfo;
+import org.apache.shiro.config.Ini;
+import org.apache.shiro.config.Ini.Section;
+import org.apache.shiro.realm.AuthorizingRealm;
+import org.apache.shiro.subject.PrincipalCollection;
+import org.killbill.billing.util.config.definition.SecurityConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.http.client.AsyncCompletionHandler;
+import com.ning.http.client.AsyncHttpClient;
+import com.ning.http.client.AsyncHttpClient.BoundRequestBuilder;
+import com.ning.http.client.AsyncHttpClientConfig;
+import com.ning.http.client.ListenableFuture;
+import com.ning.http.client.Response;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+public class KillBillOktaRealm extends AuthorizingRealm {
+
+ private static final Logger log = LoggerFactory.getLogger(KillBillOktaRealm.class);
+ private static final ObjectMapper mapper = new ObjectMapper();
+ private static final int DEFAULT_TIMEOUT_SECS = 15;
+ private static final Splitter SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults();
+
+ private final Map<String, Collection<String>> permissionsByGroup = Maps.newLinkedHashMap();
+
+ private final SecurityConfig securityConfig;
+ private final AsyncHttpClient httpClient;
+
+ @Inject
+ public KillBillOktaRealm(final SecurityConfig securityConfig) {
+ this.securityConfig = securityConfig;
+ this.httpClient = new AsyncHttpClient(new AsyncHttpClientConfig.Builder().setRequestTimeout(DEFAULT_TIMEOUT_SECS * 1000).build());
+
+ if (securityConfig.getShiroOktaPermissionsByGroup() != null) {
+ final Ini ini = new Ini();
+ // When passing properties on the command line, \n can be escaped
+ ini.load(securityConfig.getShiroOktaPermissionsByGroup().replace("\\n", "\n"));
+ for (final Section section : ini.getSections()) {
+ for (final String role : section.keySet()) {
+ final Collection<String> permissions = ImmutableList.<String>copyOf(SPLITTER.split(section.get(role)));
+ permissionsByGroup.put(role, permissions);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {
+ final String username = (String) getAvailablePrincipal(principals);
+ final String userId = findOktaUserId(username);
+ final Set<String> userGroups = findOktaGroupsForUser(userId);
+
+ final SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(userGroups);
+ final Set<String> stringPermissions = groupsPermissions(userGroups);
+ simpleAuthorizationInfo.setStringPermissions(stringPermissions);
+
+ return simpleAuthorizationInfo;
+ }
+
+ @Override
+ protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token) throws AuthenticationException {
+ final UsernamePasswordToken upToken = (UsernamePasswordToken) token;
+ if (doAuthenticate(upToken)) {
+ // Credentials are valid
+ return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), getName());
+ } else {
+ throw new AuthenticationException("Okta authentication failed");
+ }
+ }
+
+ private boolean doAuthenticate(final UsernamePasswordToken upToken) {
+ final BoundRequestBuilder builder = httpClient.preparePost(securityConfig.getShiroOktaUrl() + "/api/v1/authn");
+ try {
+ final ImmutableMap<String, String> body = ImmutableMap.<String, String>of("username", upToken.getUsername(),
+ "password", String.valueOf(upToken.getPassword()));
+ builder.setBody(mapper.writeValueAsString(body));
+ } catch (final JsonProcessingException e) {
+ log.warn("Error while generating Okta payload");
+ throw new AuthenticationException(e);
+ }
+ builder.addHeader("Authorization", "SSWS " + securityConfig.getShiroOktaAPIToken());
+ builder.addHeader("Content-Type", "application/json; charset=UTF-8");
+ final Response response;
+ try {
+ final ListenableFuture<Response> futureStatus =
+ builder.execute(new AsyncCompletionHandler<Response>() {
+ @Override
+ public Response onCompleted(final Response response) throws Exception {
+ return response;
+ }
+ });
+ response = futureStatus.get(DEFAULT_TIMEOUT_SECS, TimeUnit.SECONDS);
+ } catch (final TimeoutException toe) {
+ log.warn("Timeout while connecting to Okta");
+ throw new AuthenticationException(toe);
+ } catch (final Exception e) {
+ log.warn("Error while connecting to Okta");
+ throw new AuthenticationException(e);
+ }
+
+ return isAuthenticated(response);
+ }
+
+ private boolean isAuthenticated(final Response oktaRawResponse) {
+ try {
+ final Map oktaResponse = mapper.readValue(oktaRawResponse.getResponseBodyAsStream(), Map.class);
+ if ("SUCCESS".equals(oktaResponse.get("status"))) {
+ return true;
+ } else {
+ log.warn("Okta authentication failed: " + oktaResponse);
+ return false;
+ }
+ } catch (final IOException e) {
+ log.warn("Unable to read response from Okta");
+ throw new AuthenticationException(e);
+ }
+ }
+
+ private String findOktaUserId(final String login) {
+ final String path;
+ try {
+ path = "/api/v1/users/" + URLEncoder.encode(login, "UTF-8");
+ } catch (final UnsupportedEncodingException e) {
+ // Should never happen
+ throw new IllegalStateException(e);
+ }
+
+ final Response oktaRawResponse = doGetRequest(path);
+ try {
+ final Map oktaResponse = mapper.readValue(oktaRawResponse.getResponseBodyAsStream(), Map.class);
+ return (String) oktaResponse.get("id");
+ } catch (final IOException e) {
+ log.warn("Unable to read response from Okta");
+ throw new AuthorizationException(e);
+ }
+ }
+
+ private Set<String> findOktaGroupsForUser(final String userId) {
+ final String path = "/api/v1/users/" + userId + "/groups";
+ final Response response = doGetRequest(path);
+ return getGroups(response);
+ }
+
+ private Response doGetRequest(final String path) {
+ final BoundRequestBuilder builder = httpClient.prepareGet(securityConfig.getShiroOktaUrl() + path);
+ builder.addHeader("Authorization", "SSWS " + securityConfig.getShiroOktaAPIToken());
+ builder.addHeader("Content-Type", "application/json; charset=UTF-8");
+ final Response response;
+ try {
+ final ListenableFuture<Response> futureStatus =
+ builder.execute(new AsyncCompletionHandler<Response>() {
+ @Override
+ public Response onCompleted(final Response response) throws Exception {
+ return response;
+ }
+ });
+ response = futureStatus.get(DEFAULT_TIMEOUT_SECS, TimeUnit.SECONDS);
+ } catch (final TimeoutException toe) {
+ log.warn("Timeout while connecting to Okta");
+ throw new AuthorizationException(toe);
+ } catch (final Exception e) {
+ log.warn("Error while connecting to Okta");
+ throw new AuthorizationException(e);
+ }
+ return response;
+ }
+
+ private Set<String> getGroups(final Response oktaRawResponse) {
+ try {
+ final List<Map> oktaResponse = mapper.readValue(oktaRawResponse.getResponseBodyAsStream(), new TypeReference<List<Map>>() {});
+ final Set<String> groups = new HashSet<String>();
+ for (final Map group : oktaResponse) {
+ final Object groupProfile = group.get("profile");
+ if (groupProfile != null && groupProfile instanceof Map) {
+ groups.add((String) ((Map) groupProfile).get("name"));
+ }
+ }
+ return groups;
+ } catch (final IOException e) {
+ log.warn("Unable to read response from Okta");
+ throw new AuthorizationException(e);
+ }
+ }
+
+ private Set<String> groupsPermissions(final Iterable<String> groups) {
+ final Set<String> permissions = new HashSet<String>();
+ for (final String group : groups) {
+ final Collection<String> permissionsForGroup = permissionsByGroup.get(group);
+ if (permissionsForGroup != null) {
+ permissions.addAll(permissionsForGroup);
+ }
+ }
+ return permissions;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/api/DefaultTagUserApi.java b/util/src/main/java/org/killbill/billing/util/tag/api/DefaultTagUserApi.java
index b461cc2..55314d1 100644
--- a/util/src/main/java/org/killbill/billing/util/tag/api/DefaultTagUserApi.java
+++ b/util/src/main/java/org/killbill/billing/util/tag/api/DefaultTagUserApi.java
@@ -75,7 +75,7 @@ public class DefaultTagUserApi implements TagUserApi {
@Override
public List<TagDefinition> getTagDefinitions(final TenantContext context) {
- return ImmutableList.<TagDefinition>copyOf(Collections2.transform(tagDefinitionDao.getTagDefinitions(false, internalCallContextFactory.createInternalTenantContextWithoutAccountRecordId(context)),
+ return ImmutableList.<TagDefinition>copyOf(Collections2.transform(tagDefinitionDao.getTagDefinitions(true, internalCallContextFactory.createInternalTenantContextWithoutAccountRecordId(context)),
new Function<TagDefinitionModelDao, TagDefinition>() {
@Override
public TagDefinition apply(final TagDefinitionModelDao input) {
diff --git a/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
index 3b5aefa..cf6ac03 100644
--- a/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
+++ b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
@@ -107,6 +107,11 @@ userAndSystemTagDefinitions() ::= <<
, \'Generate account invoices in DRAFT mode.\' description
union
select
+ \'00000000-0000-0000-0000-000000000009\' id
+ , \'AUTO_INVOICING_REUSE_DRAFT\' as name
+ , \'Use existing draft invoice if exists.\' description
+ union
+ select
\'00000000-0000-0001-0000-000000000001\' id
, \'__PARK__\' as name
, \'Accounts with invalid invoicing state\' description
diff --git a/util/src/test/java/org/killbill/billing/mock/api/MockExtBusEvent.java b/util/src/test/java/org/killbill/billing/mock/api/MockExtBusEvent.java
index f11209e..591327f 100644
--- a/util/src/test/java/org/killbill/billing/mock/api/MockExtBusEvent.java
+++ b/util/src/test/java/org/killbill/billing/mock/api/MockExtBusEvent.java
@@ -78,6 +78,11 @@ public class MockExtBusEvent implements ExtBusEvent {
}
@Override
+ public UUID getUserToken() {
+ return null;
+ }
+
+ @Override
public String toString() {
return "MockExtBusEvent [eventType=" + eventType + ", objectType="
+ objectType + ", objectId=" + objectId + ", accountId="
diff --git a/util/src/test/java/org/killbill/billing/mock/MockPlan.java b/util/src/test/java/org/killbill/billing/mock/MockPlan.java
index dc78dea..d02457c 100644
--- a/util/src/test/java/org/killbill/billing/mock/MockPlan.java
+++ b/util/src/test/java/org/killbill/billing/mock/MockPlan.java
@@ -21,6 +21,7 @@ import java.util.Iterator;
import java.util.UUID;
import org.joda.time.DateTime;
+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.PhaseType;
@@ -43,6 +44,11 @@ public class MockPlan implements Plan {
}
@Override
+ public BillingMode getRecurringBillingMode() {
+ return BillingMode.IN_ADVANCE;
+ }
+
+ @Override
public PlanPhase[] getInitialPhases() {
throw new UnsupportedOperationException();
}
diff --git a/util/src/test/java/org/killbill/billing/mock/MockSubscription.java b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
index c353b0d..146a130 100644
--- a/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
+++ b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
@@ -27,6 +27,7 @@ import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.Product;
@@ -83,18 +84,18 @@ public class MockSubscription implements SubscriptionBase {
}
@Override
- public DateTime changePlan(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final CallContext context) throws SubscriptionBaseApiException {
+ public DateTime changePlan(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final CallContext context) throws SubscriptionBaseApiException {
return sub.changePlan(spec, overrides, context);
}
@Override
- public DateTime changePlanWithDate(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDate,
+ public DateTime changePlanWithDate(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDate,
final CallContext context) throws SubscriptionBaseApiException {
return sub.changePlanWithDate(spec, overrides, requestedDate, context);
}
@Override
- public DateTime changePlanWithPolicy(final PlanSpecifier spec,
+ public DateTime changePlanWithPolicy(final PlanPhaseSpecifier spec,
final List<PlanPhasePriceOverride> overrides, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
return sub.changePlanWithPolicy(spec, overrides, policy, context);
}
diff --git a/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillOktaRealm.java b/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillOktaRealm.java
new file mode 100644
index 0000000..edc374a
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillOktaRealm.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * 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.security.shiro.realm;
+
+import java.util.Properties;
+
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.authz.AuthorizationInfo;
+import org.apache.shiro.subject.SimplePrincipalCollection;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.util.config.definition.SecurityConfig;
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+import org.skife.config.SimplePropertyConfigSource;
+import org.testng.annotations.Test;
+
+public class TestKillBillOktaRealm extends UtilTestSuiteNoDB {
+
+ @Test(groups = "external", enabled = false)
+ public void testCheckOktaConnection() throws Exception {
+ // Convenience method to verify your Okta connectivity
+ final Properties props = new Properties();
+ props.setProperty("org.killbill.security.okta.url", "https://dev-XXXXXX.oktapreview.com");
+ props.setProperty("org.killbill.security.okta.apiToken", "YYYYYY");
+ props.setProperty("org.killbill.security.okta.permissionsByGroup", "support-group: entitlement:*\n" +
+ "finance-group: invoice:*, payment:*\n" +
+ "ops-group: *:*");
+ final ConfigSource customConfigSource = new SimplePropertyConfigSource(props);
+ final SecurityConfig securityConfig = new ConfigurationObjectFactory(customConfigSource).build(SecurityConfig.class);
+ final KillBillOktaRealm oktaRealm = new KillBillOktaRealm(securityConfig);
+
+ final String username = "pierre";
+ final String password = "password";
+
+ // Check authentication
+ final UsernamePasswordToken token = new UsernamePasswordToken(username, password);
+ final AuthenticationInfo authenticationInfo = oktaRealm.getAuthenticationInfo(token);
+ System.out.println(authenticationInfo);
+
+ // Check permissions
+ final SimplePrincipalCollection principals = new SimplePrincipalCollection(username, username);
+ final AuthorizationInfo authorizationInfo = oktaRealm.doGetAuthorizationInfo(principals);
+ System.out.println("Roles: " + authorizationInfo.getRoles());
+ System.out.println("Permissions: " + authorizationInfo.getStringPermissions());
+ }
+}